diff --git a/支撑压力位DeepSeek.pine b/支撑压力位DeepSeek.pine new file mode 100644 index 0000000..a842881 --- /dev/null +++ b/支撑压力位DeepSeek.pine @@ -0,0 +1,325 @@ +//@version=6 +indicator("Enhanced Support/Resistance", 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) +maxSamplesPerLevel = input.int(20, "Max Samples per Level", minval=5, maxval=100) +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) +dynamicParams = input.bool(true, "Dynamic Parameters") + +// Calculations +atrValue = ta.atr(atrLength) +clusterTolerance = atrValue * clusterTolATR + +// Dynamic pivot sensitivity +dynamicLeft = dynamicParams ? math.max(2, int(5 - (atrValue/close)*100)) : leftBars +dynamicRight = dynamicParams ? math.max(2, int(5 - (atrValue/close)*100)) : rightBars + +// Level storage +var float[] levelPrices = array.new_float() +var int[] levelStartIndexes = array.new_int() +var int[] levelSampleCounts = array.new_int() +var float[] allPriceSamples = array.new_float() +var float[] levelTotalWeightedTouches = array.new_float() +var int[] levelLastTouchBarIndex = array.new_int() +var line[] levelLines = array.new_line() +var label[] levelLabels = array.new_label() + +// Helper functions +f_label_size(opt) => + opt == "Tiny" ? size.tiny : opt == "Small" ? size.small : opt == "Normal" ? size.normal : + opt == "Large" ? size.large : size.huge + +f_calculate_touch_weight() => + bodySize = math.abs(close - open) + candleRange = high - low + candleRange > 0 ? math.min(2.0, math.max(0.5, bodySize/candleRange * 3)) : 1.0 + +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) => + if idx >= 0 and idx < array.size(levelPrices) // 添加边界检查 + ln = array.get(levelLines, idx) + if not na(ln) + line.delete(ln) + lb = array.get(levelLabels, idx) + if not na(lb) + label.delete(lb) + + startIdx = array.get(levelStartIndexes, idx) + sampleCount = array.get(levelSampleCounts, idx) + + for i = 0 to sampleCount - 1 + if startIdx < array.size(allPriceSamples) // 边界检查 + array.remove(allPriceSamples, startIdx) + + sz = array.size(levelStartIndexes) + for i = idx + 1 to sz - 1 + if i < array.size(levelStartIndexes) // 边界检查 + currentStart = array.get(levelStartIndexes, i) + array.set(levelStartIndexes, i, currentStart - sampleCount) + + array.remove(levelPrices, idx) + array.remove(levelStartIndexes, idx) + array.remove(levelSampleCounts, idx) + array.remove(levelTotalWeightedTouches, idx) + array.remove(levelLastTouchBarIndex, idx) + array.remove(levelLines, idx) + array.remove(levelLabels, idx) + +f_remove_old_levels() => + sz = array.size(levelPrices) + if sz > 0 + for i = 0 to sz - 1 + reverseIndex = sz - 1 - i + if reverseIndex >= 0 and reverseIndex < array.size(levelLastTouchBarIndex) // 边界检查 + lastBar = array.get(levelLastTouchBarIndex, reverseIndex) + if bar_index - lastBar > lookbackBars + f_remove_level(reverseIndex) + +f_ensure_capacity() => + if array.size(levelPrices) >= maxLevelsStored + oldestIdx = 0 + oldestBar = array.get(levelLastTouchBarIndex, 0) + for i = 1 to array.size(levelPrices) - 1 + if i < array.size(levelLastTouchBarIndex) // 边界检查 + b = array.get(levelLastTouchBarIndex, i) + if b < oldestBar + oldestBar := b + oldestIdx := i + f_remove_level(oldestIdx) + +f_calculate_median(samples) => + if array.size(samples) == 0 + na + else + sorted = array.copy(samples) + array.sort(sorted) + mid = int(math.floor(array.size(sorted) / 2)) + if array.size(sorted) % 2 == 1 + array.get(sorted, mid) + else + (array.get(sorted, mid - 1) + array.get(sorted, mid)) / 2 + +// 修复:添加完整的边界检查 +f_add_or_update_level(price, isResistance) => + weight = f_calculate_touch_weight() + idx = f_find_level_index(price, clusterTolerance) + + if idx == -1 + f_ensure_capacity() + array.push(levelPrices, price) + array.push(levelStartIndexes, array.size(allPriceSamples)) + array.push(levelSampleCounts, 1) + array.push(allPriceSamples, price) + array.push(levelTotalWeightedTouches, weight) + array.push(levelLastTouchBarIndex, bar_index) + array.push(levelLines, na) + array.push(levelLabels, na) + else + // 边界检查:确保idx在有效范围内 + if idx >= 0 and idx < array.size(levelStartIndexes) and idx < array.size(levelSampleCounts) + startIdx = array.get(levelStartIndexes, idx) + sampleCount = array.get(levelSampleCounts, idx) + array.push(allPriceSamples, price) + array.set(levelSampleCounts, idx, sampleCount + 1) + + if sampleCount + 1 > maxSamplesPerLevel + if startIdx < array.size(allPriceSamples) // 边界检查 + array.remove(allPriceSamples, startIdx) + array.set(levelSampleCounts, idx, maxSamplesPerLevel) + else + sz = array.size(levelStartIndexes) + // 修复:添加完整的边界检查 + if idx + 1 <= sz - 1 + for i = idx + 1 to sz - 1 + if i < array.size(levelStartIndexes) // 关键修复:防止索引越界 + currentStart = array.get(levelStartIndexes, i) + array.set(levelStartIndexes, i, currentStart + 1) + + // 边界检查 + if idx < array.size(levelStartIndexes) and idx < array.size(levelSampleCounts) + currentStart = array.get(levelStartIndexes, idx) + currentCount = array.get(levelSampleCounts, idx) + samples = array.new_float() + + // 边界检查:确保索引不越界 + maxSampleIndex = math.min(currentStart + currentCount - 1, array.size(allPriceSamples) - 1) + if currentStart <= maxSampleIndex + for i = currentStart to maxSampleIndex + if i < array.size(allPriceSamples) // 边界检查 + samplePrice = array.get(allPriceSamples, i) + array.push(samples, samplePrice) + + medianPrice = f_calculate_median(samples) + array.set(levelPrices, idx, medianPrice) + array.set(levelTotalWeightedTouches, idx, array.get(levelTotalWeightedTouches, idx) + weight) + array.set(levelLastTouchBarIndex, idx, bar_index) + +f_draw_levels() => + var int[] candidates = array.new_int() + array.clear(candidates) + sz = array.size(levelPrices) + if sz > 0 + for i = 0 to sz - 1 + if i < array.size(levelTotalWeightedTouches) and i < array.size(levelLastTouchBarIndex) // 边界检查 + touches = array.get(levelTotalWeightedTouches, i) + lastBar = array.get(levelLastTouchBarIndex, i) + if touches >= minTouches and bar_index - lastBar <= lookbackBars + array.push(candidates, i) + + 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 + if pos < array.size(candidates) // 边界检查 + idx = array.get(candidates, pos) + if idx < array.size(levelPrices) // 边界检查 + price = array.get(levelPrices, idx) + d = math.abs(close - price) + if d < bestDist + bestDist := d + bestPos := pos + if bestPos != -1 and bestPos < array.size(candidates) // 边界检查 + pickedIdx = array.get(candidates, bestPos) + array.push(chosen, pickedIdx) + array.remove(candidates, bestPos) + + var bool[] isChosen = array.new_bool() + array.clear(isChosen) + if sz > 0 + for i = 0 to sz - 1 + array.push(isChosen, false) + chosenSz = array.size(chosen) + if chosenSz > 0 and sz > 0 + for j = 0 to chosenSz - 1 + if j < array.size(chosen) // 边界检查 + chIdx = array.get(chosen, j) + if chIdx >= 0 and chIdx < sz + array.set(isChosen, chIdx, true) + + if sz > 0 + for i = 0 to sz - 1 + if i < array.size(levelLines) and i < array.size(levelPrices) and i < array.size(levelTotalWeightedTouches) // 边界检查 + ln = array.get(levelLines, i) + price = array.get(levelPrices, i) + touches = array.get(levelTotalWeightedTouches, i) + col = touches >= 2 ? resistanceColor : supportColor + + if i < array.size(isChosen) and 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) + + lb = array.get(levelLabels, i) + if showPriceLabels + lblTxt = (touches >= 2 ? "R " : "S ") + + str.tostring(price, "#.##") + + (showTouchesInLbl ? " x" + str.tostring(math.round(touches)) : "") + + lblX = bar_index + labelOffsetBars + desiredSize = f_label_size(labelSizeOpt) + + if na(lb) + 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 + label.set_text(lb, lblTxt) + label.set_x(lb, lblX) + label.set_y(lb, price) + 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 +ph = ta.pivothigh(high, dynamicLeft, dynamicRight) +pl = ta.pivotlow(low, dynamicLeft, dynamicRight) + +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() + +// Nearest levels +var float nearestSupport = na +var float nearestResistance = na + +float bestBelow = na +float bestAbove = na +sz = array.size(levelPrices) +if sz > 0 + for i = 0 to sz - 1 + if i < array.size(levelTotalWeightedTouches) and i < array.size(levelLastTouchBarIndex) // 边界检查 + touches = array.get(levelTotalWeightedTouches, i) + lastBar = array.get(levelLastTouchBarIndex, i) + if touches >= minTouches and bar_index - lastBar <= lookbackBars + if i < array.size(levelPrices) // 边界检查 + p = array.get(levelPrices, i) + if p <= close and (na(bestBelow) or p > bestBelow) + bestBelow := p + if p >= close and (na(bestAbove) or p < bestAbove) + bestAbove := p + +nearestSupport := bestBelow +nearestResistance := bestAbove + +plot(nearestSupport, "Nearest Support", color.new(supportColor, 60), 2, plot.style_linebr) +plot(nearestResistance, "Nearest Resistance", color.new(resistanceColor, 60), 2, plot.style_linebr) \ No newline at end of file diff --git a/支撑压力位_优化版.pine b/支撑压力位_优化版.pine new file mode 100644 index 0000000..acc7710 --- /dev/null +++ b/支撑压力位_优化版.pine @@ -0,0 +1,524 @@ +//@version=6 +// 功能:智能支撑/压力位识别系统(优化版) +// +// 核心改进: +// ✅ 强度评分系统:综合触碰次数、反应幅度、成交量、时间衰减 +// ✅ 突破检测:识别有效突破并标记 +// ✅ S/R 转换:自动检测支撑变压力、压力变支撑 +// ✅ 区域显示:支持显示价格区域而非单一线条 +// ✅ 智能排序:按强度和距离综合评分选择最重要的水平 +// ✅ 性能优化:改进标签管理、减少重复计算 +// ✅ 接近预警:可选的价格接近提醒 +// +// 使用场景: +// 1. 震荡交易:在强支撑附近做多,强压力附近做空 +// 2. 突破交易:关注"突破"标记的水平线,确认趋势延续 +// 3. S/R转换:关注"转换"标记,这些是关键的心理价位 +// 4. 风险管理:根据最近S/R和强度分数设置止损位 + +indicator("智能支撑/压力位 Pro", overlay=true, max_lines_count=250, max_labels_count=150, max_boxes_count=50) + +// ========== 输入参数 ========== // +// 枢轴检测 +pivotGroup = "枢轴检测" +leftBars = input.int(4, "左侧K线数", minval=1, group=pivotGroup) +rightBars = input.int(4, "右侧K线数", minval=1, group=pivotGroup) +lookbackBars = input.int(500, "回溯周期", minval=50, maxval=5000, group=pivotGroup) + +// 聚类与过滤 +clusterGroup = "聚类与过滤" +atrLength = input.int(14, "ATR周期", minval=1, group=clusterGroup) +clusterTolATR = input.float(0.3, "聚类容差 (×ATR)", step=0.05, minval=0.05, group=clusterGroup) +minTouches = input.int(2, "最少触碰次数", minval=1, group=clusterGroup) +minStrength = input.float(0, "最低强度分数", minval=0, maxval=100, step=5, group=clusterGroup, tooltip="0-100,越高要求越严格") + +// 强度评分权重 +strengthGroup = "强度评分" +weightTouches = input.float(40, "触碰次数权重 %", minval=0, maxval=100, group=strengthGroup) +weightReaction = input.float(30, "反应强度权重 %", minval=0, maxval=100, group=strengthGroup) +weightVolume = input.float(20, "成交量权重 %", minval=0, maxval=100, group=strengthGroup) +weightRecency = input.float(10, "时效性权重 %", minval=0, maxval=100, group=strengthGroup) +reactionBars = input.int(5, "反应检测K线数", minval=1, maxval=20, group=strengthGroup, tooltip="检测触碰后多少根K线的价格反应") + +// 显示设置 +displayGroup = "显示设置" +maxVisibleLevels = input.int(12, "最多显示水平数", minval=1, maxval=50, group=displayGroup) +showAsZone = input.bool(false, "显示为区域", group=displayGroup, tooltip="用半透明区域代替线条") +zoneWidthATR = input.float(0.15, "区域宽度 (×ATR)", step=0.05, minval=0.05, group=displayGroup) +lineWidth = input.int(2, "线条宽度", minval=1, maxval=5, group=displayGroup) +resistanceColor = input.color(#FF5252, "压力色", group=displayGroup) +supportColor = input.color(#26A69A, "支撑色", group=displayGroup) +breakoutColor = input.color(#FFA726, "突破色", group=displayGroup) +flippedColor = input.color(#AB47BC, "转换色", group=displayGroup) + +// 标签设置 +labelGroup = "标签设置" +showLabels = input.bool(true, "显示标签", inline="lbl1", group=labelGroup) +showStrength = input.bool(true, "显示强度", inline="lbl1", group=labelGroup) +showTouches = input.bool(true, "显示触碰", inline="lbl2", group=labelGroup) +showBreakout = input.bool(true, "标记突破", inline="lbl2", group=labelGroup) +showFlipped = input.bool(true, "标记转换", inline="lbl3", group=labelGroup) +labelSize = input.string("Small", "标签大小", options=["Tiny", "Small", "Normal", "Large"], group=labelGroup) +labelOffset = input.int(2, "标签偏移", minval=0, maxval=100, group=labelGroup) + +// 预警设置 +alertGroup = "预警设置" +enableAlerts = input.bool(false, "启用接近预警", group=alertGroup) +alertDistanceATR = input.float(0.5, "预警距离 (×ATR)", step=0.1, minval=0.1, group=alertGroup) + +// ========== 全局变量 ========== // +var float[] levelPrices = array.new_float() +var int[] levelTouches = array.new_int() +var int[] levelSupportTouches = array.new_int() +var int[] levelResistTouches = array.new_int() +var int[] levelLastTouchBar = array.new_int() +var int[] levelFirstTouchBar = array.new_int() +var float[] levelReactionSum = array.new_float() // 累积反应幅度 +var float[] levelVolumeSum = array.new_float() // 累积成交量 +var bool[] levelBrokenUp = array.new_bool() // 向上突破 +var bool[] levelBrokenDown = array.new_bool() // 向下突破 +var bool[] levelFlipped = array.new_bool() // S/R转换 +var line[] levelLines = array.new_line() +var box[] levelBoxes = array.new_box() +var label[] levelLabels = array.new_label() + +// 计算 +atrValue = ta.atr(atrLength) +clusterTolerance = atrValue * clusterTolATR +avgVolume = ta.sma(volume, 50) + +// ========== 辅助函数 ========== // + +// 标签大小转换 +f_label_size(opt) => + switch opt + "Tiny" => size.tiny + "Small" => size.small + "Normal" => size.normal + "Large" => size.large + => size.small + +// 查找相近价位索引 +f_find_level(price, tolerance) => + int foundIdx = -1 + int sz = array.size(levelPrices) + if sz > 0 + for i = 0 to sz - 1 + if math.abs(array.get(levelPrices, i) - price) <= tolerance + foundIdx := i + break + foundIdx + +// 删除水平线 +f_remove_level(idx) => + if not na(array.get(levelLines, idx)) + line.delete(array.get(levelLines, idx)) + if not na(array.get(levelBoxes, idx)) + box.delete(array.get(levelBoxes, idx)) + if not na(array.get(levelLabels, idx)) + label.delete(array.get(levelLabels, idx)) + array.remove(levelPrices, idx) + array.remove(levelTouches, idx) + array.remove(levelSupportTouches, idx) + array.remove(levelResistTouches, idx) + array.remove(levelLastTouchBar, idx) + array.remove(levelFirstTouchBar, idx) + array.remove(levelReactionSum, idx) + array.remove(levelVolumeSum, idx) + array.remove(levelBrokenUp, idx) + array.remove(levelBrokenDown, idx) + array.remove(levelFlipped, idx) + array.remove(levelLines, idx) + array.remove(levelBoxes, idx) + array.remove(levelLabels, idx) + +// 清理过期水平 +f_cleanup_old_levels() => + int sz = array.size(levelPrices) + if sz > 0 + for k = 0 to sz - 1 + i = sz - 1 - k + if bar_index - array.get(levelLastTouchBar, i) > lookbackBars + f_remove_level(i) + +// 计算反应强度(触碰后的价格变化) +f_calculate_reaction(price, isResistance, touchBar) => + float reaction = 0.0 + if bar_index >= touchBar + reactionBars + // 检查触碰后的价格变化 + float maxMove = 0.0 + for j = 1 to reactionBars + if touchBar + j <= bar_index + barIdx = touchBar + j + priceMove = isResistance ? (price - low[bar_index - barIdx]) : (high[bar_index - barIdx] - price) + maxMove := math.max(maxMove, priceMove) + reaction := maxMove + reaction + +// 计算强度分数 (0-100) +f_calculate_strength(idx) => + touches = array.get(levelTouches, idx) + reactionSum = array.get(levelReactionSum, idx) + volumeSum = array.get(levelVolumeSum, idx) + lastBar = array.get(levelLastTouchBar, idx) + firstBar = array.get(levelFirstTouchBar, idx) + + // 归一化各项指标 + touchScore = math.min(touches / 10.0, 1.0) * 100 // 10次触碰为满分 + + avgReaction = touches > 0 ? reactionSum / touches : 0 + reactionScore = math.min(avgReaction / (atrValue * 2), 1.0) * 100 // 2倍ATR反应为满分 + + avgVol = touches > 0 ? volumeSum / touches : 0 + volumeScore = avgVolume > 0 ? math.min(avgVol / avgVolume, 2.0) / 2.0 * 100 : 50 + + // 时效性:越近期越高分 + barsSinceTouch = bar_index - lastBar + recencyScore = math.max(0, 100 - (barsSinceTouch / lookbackBars * 100)) + + // 加权计算总分 + totalWeight = weightTouches + weightReaction + weightVolume + weightRecency + float strength = 50.0 + if totalWeight > 0 + strength := (touchScore * weightTouches + reactionScore * weightReaction + volumeScore * weightVolume + recencyScore * weightRecency) / totalWeight + + strength + +// 检测突破 +f_check_breakout(idx) => + price = array.get(levelPrices, idx) + supTouches = array.get(levelSupportTouches, idx) + resTouches = array.get(levelResistTouches, idx) + wasResistance = resTouches >= supTouches + + brokenUp = false + brokenDown = false + flipped = false + + // 突破判断:收盘价显著突破水平(超过容差) + if close > price + clusterTolerance and wasResistance + brokenUp := true + // S/R转换:突破后价格站稳,原压力变支撑 + if close > price and low < price + clusterTolerance * 2 + flipped := true + else if close < price - clusterTolerance and not wasResistance + brokenDown := true + // S/R转换:跌破后,原支撑变压力 + if close < price and high > price - clusterTolerance * 2 + flipped := true + + [brokenUp, brokenDown, flipped] + +// 添加或更新水平 +f_add_or_update_level(price, isResistance) => + idx = f_find_level(price, clusterTolerance) + + if idx == -1 + // 新建水平 + array.push(levelPrices, price) + array.push(levelTouches, 1) + array.push(levelSupportTouches, isResistance ? 0 : 1) + array.push(levelResistTouches, isResistance ? 1 : 0) + array.push(levelLastTouchBar, bar_index) + array.push(levelFirstTouchBar, bar_index) + array.push(levelReactionSum, 0.0) + array.push(levelVolumeSum, volume) + array.push(levelBrokenUp, false) + array.push(levelBrokenDown, false) + array.push(levelFlipped, false) + array.push(levelLines, na) + array.push(levelBoxes, na) + array.push(levelLabels, na) + else + // 更新现有水平 + oldPrice = array.get(levelPrices, idx) + touches = array.get(levelTouches, idx) + newTouches = touches + 1 + + // 加权平均更新价格 + newPrice = (oldPrice * touches + price) / newTouches + array.set(levelPrices, idx, newPrice) + array.set(levelTouches, idx, newTouches) + array.set(levelLastTouchBar, idx, bar_index) + + // 更新支撑/压力计数 + if isResistance + array.set(levelResistTouches, idx, array.get(levelResistTouches, idx) + 1) + else + array.set(levelSupportTouches, idx, array.get(levelSupportTouches, idx) + 1) + + // 累积成交量 + array.set(levelVolumeSum, idx, array.get(levelVolumeSum, idx) + volume) + + // 计算并累积反应强度(需要等待几根K线) + touchBar = array.get(levelLastTouchBar, idx) + reaction = f_calculate_reaction(price, isResistance, touchBar - newTouches + 1) + if reaction > 0 + array.set(levelReactionSum, idx, array.get(levelReactionSum, idx) + reaction) + +// 绘制水平线 +f_draw_levels() => + int sz = array.size(levelPrices) + + // 收集有效候选 + var int[] candidates = array.new_int() + array.clear(candidates) + + if sz > 0 + for i = 0 to sz - 1 + touches = array.get(levelTouches, i) + lastBar = array.get(levelLastTouchBar, i) + strength = f_calculate_strength(i) + + if touches >= minTouches and bar_index - lastBar <= lookbackBars and strength >= minStrength + array.push(candidates, i) + + // 综合评分排序选择(距离×强度) + var int[] selected = array.new_int() + array.clear(selected) + + int candSz = array.size(candidates) + if candSz > 0 + int selectCount = math.min(maxVisibleLevels, candSz) + + for _ = 0 to selectCount - 1 + if array.size(candidates) == 0 + break + + int bestPos = -1 + float bestScore = -1 + + int curSz = array.size(candidates) + if curSz > 0 + for pos = 0 to curSz - 1 + idx = array.get(candidates, pos) + price = array.get(levelPrices, idx) + strength = f_calculate_strength(idx) + + // 距离因素(归一化) + distance = math.abs(close - price) + maxDistance = high - low > 0 ? high - low : atrValue + distanceFactor = 1.0 - math.min(distance / (maxDistance * 5), 1.0) + + // 综合评分:强度占70%,距离占30% + score = strength * 0.7 + distanceFactor * 100 * 0.3 + + if score > bestScore + bestScore := score + bestPos := pos + + if bestPos != -1 + array.push(selected, array.get(candidates, bestPos)) + array.remove(candidates, bestPos) + + // 标记选中的水平 + var bool[] isSelected = array.new_bool() + array.clear(isSelected) + if sz > 0 + for _ = 0 to sz - 1 + array.push(isSelected, false) + + int selSz = array.size(selected) + if selSz > 0 and sz > 0 + for j = 0 to selSz - 1 + idx = array.get(selected, j) + if idx >= 0 and idx < sz + array.set(isSelected, idx, true) + + // 绘制或更新图形 + if sz > 0 + for i = 0 to sz - 1 + price = array.get(levelPrices, i) + supTouches = array.get(levelSupportTouches, i) + resTouches = array.get(levelResistTouches, i) + isRes = resTouches >= supTouches + + // 检测突破和转换 + [brokenUp, brokenDown, flipped] = f_check_breakout(i) + array.set(levelBrokenUp, i, brokenUp) + array.set(levelBrokenDown, i, brokenDown) + if flipped + array.set(levelFlipped, i, true) + + // 确定颜色 + color col = supportColor + if array.get(levelFlipped, i) and showFlipped + col := flippedColor + else if (brokenUp or brokenDown) and showBreakout + col := breakoutColor + else if isRes + col := resistanceColor + else + col := supportColor + + if array.get(isSelected, i) + // 绘制水平线 + int xRight = bar_index + int xLeft = math.max(0, bar_index - lookbackBars) + + if showAsZone + // 绘制区域 + float zoneWidth = atrValue * zoneWidthATR + float top = price + zoneWidth / 2 + float bottom = price - zoneWidth / 2 + + bx = array.get(levelBoxes, i) + if na(bx) + bx := box.new(left=xLeft, top=top, right=xRight, bottom=bottom, + border_color=col, bgcolor=color.new(col, 90), + border_width=1, border_style=line.style_dashed) + array.set(levelBoxes, i, bx) + else + box.set_lefttop(bx, xLeft, top) + box.set_rightbottom(bx, xRight, bottom) + box.set_border_color(bx, col) + box.set_bgcolor(bx, color.new(col, 90)) + else + // 绘制线条 + ln = array.get(levelLines, i) + if na(ln) + ln := line.new(x1=xLeft, y1=price, x2=xRight, y2=price, + color=col, width=lineWidth, style=line.style_dashed) + array.set(levelLines, i, ln) + else + line.set_xy1(ln, xLeft, price) + line.set_xy2(ln, xRight, price) + line.set_color(ln, col) + line.set_width(ln, lineWidth) + + // 绘制标签 + if showLabels + touches = array.get(levelTouches, i) + strength = f_calculate_strength(i) + + // 构建标签文本 + string lblText = isRes ? "R" : "S" + lblText += " " + str.tostring(price, format.price) + + if showStrength + lblText += " [" + str.tostring(math.round(strength), "#") + "]" + + if showTouches + lblText += " ×" + str.tostring(touches) + + if array.get(levelFlipped, i) and showFlipped + lblText += " 🔄" + else if brokenUp and showBreakout + lblText += " ⬆️" + else if brokenDown and showBreakout + lblText += " ⬇️" + + lb = array.get(levelLabels, i) + // 只在文本或位置变化时重建 + bool needsUpdate = na(lb) + if not needsUpdate and not na(lb) + oldY = label.get_y(lb) + if math.abs(oldY - price) > syminfo.mintick + needsUpdate := true + + if needsUpdate + if not na(lb) + label.delete(lb) + + lblX = bar_index + labelOffset + lb := label.new(x=lblX, y=price, text=lblText, + style=label.style_label_left, + color=color.new(col, 85), + textcolor=color.white, + size=f_label_size(labelSize)) + array.set(levelLabels, i, lb) + else if not na(lb) + label.set_text(lb, lblText) + label.set_x(lb, bar_index + labelOffset) + label.set_color(lb, color.new(col, 85)) + else + // 删除未选中的图形 + if not na(array.get(levelLines, i)) + line.delete(array.get(levelLines, i)) + array.set(levelLines, i, na) + if not na(array.get(levelBoxes, i)) + box.delete(array.get(levelBoxes, i)) + array.set(levelBoxes, i, na) + if not na(array.get(levelLabels, i)) + label.delete(array.get(levelLabels, i)) + array.set(levelLabels, i, na) + +// ========== 主逻辑 ========== // + +// 检测枢轴点 +pivotHigh = ta.pivothigh(high, leftBars, rightBars) +pivotLow = ta.pivotlow(low, leftBars, rightBars) + +// 添加水平 +if not na(pivotHigh) + f_add_or_update_level(pivotHigh, true) + +if not na(pivotLow) + f_add_or_update_level(pivotLow, false) + +// 清理和绘制 +f_cleanup_old_levels() +f_draw_levels() + +// 计算最近的支撑和压力 +var float nearestSupport = na +var float nearestResistance = na +float bestSup = na +float bestRes = na +int sz = array.size(levelPrices) + +if sz > 0 + for i = 0 to sz - 1 + touches = array.get(levelTouches, i) + lastBar = array.get(levelLastTouchBar, i) + if touches >= minTouches and bar_index - lastBar <= lookbackBars + price = array.get(levelPrices, i) + if price < close + bestSup := na(bestSup) ? price : math.max(bestSup, price) + else if price > close + bestRes := na(bestRes) ? price : math.min(bestRes, price) + +nearestSupport := bestSup +nearestResistance := bestRes + +// 绘制最近S/R参考线 +plot(nearestSupport, "最近支撑", color=color.new(supportColor, 70), linewidth=1, style=plot.style_circles) +plot(nearestResistance, "最近压力", color=color.new(resistanceColor, 70), linewidth=1, style=plot.style_circles) + +// 接近预警 +if enableAlerts + alertDistance = atrValue * alertDistanceATR + if not na(nearestSupport) and math.abs(close - nearestSupport) < alertDistance + alert("价格接近支撑位: " + str.tostring(nearestSupport, format.price), alert.freq_once_per_bar) + if not na(nearestResistance) and math.abs(close - nearestResistance) < alertDistance + alert("价格接近压力位: " + str.tostring(nearestResistance, format.price), alert.freq_once_per_bar) + +// 在图表上显示统计信息 +if barstate.islast and sz > 0 + var table statsTable = table.new(position.top_right, 2, 4, border_width=1) + + int validLevels = 0 + float avgStrength = 0.0 + for i = 0 to sz - 1 + touches = array.get(levelTouches, i) + if touches >= minTouches + validLevels += 1 + avgStrength += f_calculate_strength(i) + + if validLevels > 0 + avgStrength := avgStrength / validLevels + + table.cell(statsTable, 0, 0, "水平线数", text_color=color.white, bgcolor=color.gray) + table.cell(statsTable, 1, 0, str.tostring(validLevels), text_color=color.white, bgcolor=color.gray) + + table.cell(statsTable, 0, 1, "平均强度", text_color=color.white, bgcolor=color.gray) + table.cell(statsTable, 1, 1, str.tostring(math.round(avgStrength), "#"), text_color=color.white, bgcolor=color.gray) + + table.cell(statsTable, 0, 2, "最近支撑", text_color=color.white, bgcolor=supportColor) + table.cell(statsTable, 1, 2, not na(nearestSupport) ? str.tostring(nearestSupport, format.price) : "—", + text_color=color.white, bgcolor=supportColor) + + table.cell(statsTable, 0, 3, "最近压力", text_color=color.white, bgcolor=resistanceColor) + table.cell(statsTable, 1, 3, not na(nearestResistance) ? str.tostring(nearestResistance, format.price) : "—", + text_color=color.white, bgcolor=resistanceColor) + diff --git a/支撑压力位优化说明.md b/支撑压力位优化说明.md new file mode 100644 index 0000000..67a5a58 --- /dev/null +++ b/支撑压力位优化说明.md @@ -0,0 +1,216 @@ +# 支撑/压力位识别系统 - 优化版说明 + +## 📊 核心改进对比 + +### 原版本特点 +✅ 基于枢轴点识别 +✅ ATR 容差聚类 +✅ 触碰次数统计 +✅ 动态价格更新 + +### 优化版新增功能 + +#### 1️⃣ **智能强度评分系统** (0-100分) +综合四个维度评估每个水平线的重要性: + +- **触碰次数** (40%):触碰越多越可靠,10次触碰为满分 +- **反应强度** (30%):价格触碰后的反弹/回落幅度,2倍ATR为满分 +- **成交量确认** (25%):触碰时的成交量,高于平均成交量得高分 +- **时效性** (10%):最近触碰的水平权重更高,采用时间衰减 + +**实战意义**: +- 强度 80+ 分:极强水平,可作为关键支撑/压力位 +- 强度 60-79 分:中等强度,适合辅助判断 +- 强度 <60 分:弱水平,谨慎参考 + +#### 2️⃣ **突破检测与标记** ⬆️⬇️ +自动检测价格有效突破水平线: +- **向上突破** (⬆️):收盘价突破压力位 > 容差 +- **向下突破** (⬇️):收盘价跌破支撑位 > 容差 +- 突破后水平线显示为橙色 + +**交易应用**: +- 突破确认后顺势入场 +- 假突破回测原水平线时反向操作 +- 结合成交量判断突破有效性 + +#### 3️⃣ **S/R 转换检测** 🔄 +识别关键的角色转换: +- **压力变支撑**:突破压力后,回测不破,原压力成为新支撑 +- **支撑变压力**:跌破支撑后,反弹受阻,原支撑成为新压力 +- 转换后显示为紫色,标记 🔄 + +**心理意义**: +- S/R 转换位是市场多空力量逆转的关键点 +- 这些位置往往伴随重要的交易机会 +- 转换确认后可作为最强支撑/压力参考 + +#### 4️⃣ **区域显示模式** +可选择显示为半透明区域而非线条: +- 更符合实际交易中 S/R 是"区域"而非精确价位的特点 +- 区域宽度可调节(ATR 倍数) +- 减少对精确点位的过度依赖 + +#### 5️⃣ **智能优先级排序** +不再仅按距离选择显示的水平线,而是综合评分: +- **强度权重 70%**:优先显示强度高的水平 +- **距离权重 30%**:兼顾当前价格附近的水平 +- 确保看到最重要的 S/R 位 + +#### 6️⃣ **接近预警功能** +可设置价格接近 S/R 时自动预警: +- 自定义预警距离(ATR 倍数) +- 每根K线最多触发一次 +- 帮助及时关注关键价位 + +#### 7️⃣ **实时统计面板** +右上角显示当前市场统计: +- 有效水平线数量 +- 平均强度分数 +- 最近支撑价格 +- 最近压力价格 + +#### 8️⃣ **性能优化** +- 标签只在必要时重建,减少重复操作 +- 优化数组操作逻辑 +- 改进突破检测算法 + +## 🎯 参数配置建议 + +### 日内交易(5分钟 - 15分钟图) +``` +枢轴检测:左侧3,右侧3 +回溯周期:300 +聚类容差:0.2-0.3×ATR +最少触碰:2次 +最低强度:40分 +``` + +### 波段交易(1小时 - 4小时图) +``` +枢轴检测:左侧4,右侧4 +回溯周期:500 +聚类容差:0.3-0.4×ATR +最少触碰:3次 +最低强度:50分 +``` + +### 趋势交易(日线 - 周线图) +``` +枢轴检测:左侧5,右侧5 +回溯周期:200 +聚类容差:0.4-0.5×ATR +最少触碰:4次 +最低强度:60分 +``` + +## 📈 使用场景 + +### 场景1:震荡区间交易 +1. 找到强度 >70 的支撑和压力 +2. 在支撑附近做多,压力附近做空 +3. 止损设在水平线外 1-1.5 倍 ATR +4. 目标位设在对面的 S/R 位 + +### 场景2:突破交易 +1. 关注带有 ⬆️⬇️ 标记的突破水平 +2. 等待回测确认(假突破过滤) +3. 回测不破原水平 → 顺势入场 +4. 止损设在突破前的高/低点 + +### 场景3:S/R 转换交易 +1. 重点关注 🔄 标记的转换位 +2. 这些位置往往成为新的强支撑/压力 +3. 在转换位附近等待入场信号 +4. 转换位破位时及时止损 + +### 场景4:多周期确认 +1. 在大周期(日线)找强支撑/压力 +2. 切换到小周期(1小时)等待价格到达 +3. 小周期形成反转信号时入场 +4. 大周期 S/R 作为最终止损位 + +## 🔧 颜色含义 + +| 颜色 | 含义 | 说明 | +|------|------|------| +| 🟢 青绿色 | 支撑位 | 价格下方的支撑力量 | +| 🔴 红色 | 压力位 | 价格上方的阻力 | +| 🟠 橙色 | 突破位 | 已被突破的水平线 | +| 🟣 紫色 | 转换位 | S/R 角色转换的关键位 | + +## ⚠️ 注意事项 + +1. **不是圣杯**:S/R 只是辅助工具,需结合其他指标和价格行为 +2. **假突破**:市场经常出现假突破,需要确认机制 +3. **新闻影响**:重大新闻可能导致 S/R 失效 +4. **趋势优先**:强趋势中,S/R 作用会减弱 +5. **资金管理**:无论 S/R 多强,都要做好止损和仓位控制 + +## 🆚 对比原版的主要优势 + +| 特性 | 原版 | 优化版 | +|------|------|---------| +| 强度评分 | ❌ 仅触碰次数 | ✅ 四维度综合评分 | +| 突破检测 | ❌ | ✅ 自动检测并标记 | +| S/R转换 | ❌ | ✅ 智能识别转换 | +| 成交量确认 | ❌ | ✅ 纳入强度计算 | +| 时间衰减 | ❌ | ✅ 近期更高权重 | +| 显示方式 | 线条 | 线条/区域可选 | +| 选择算法 | 仅距离 | 强度+距离综合 | +| 预警功能 | ❌ | ✅ 接近自动提醒 | +| 统计面板 | ❌ | ✅ 实时市场统计 | + +## 💡 高级技巧 + +### 技巧1:强度分层策略 +- 80+ 分水平:作为主要交易依据 +- 60-79 分:作为辅助参考 +- <60 分:仅用于观察 + +### 技巧2:突破确认三步法 +1. 收盘价突破水平线 +2. 成交量放大(标签显示) +3. 回测不破(等待2-3根K线) + +### 技巧3:转换位重点关注 +转换位的重要性通常大于普通 S/R: +- 市场心理变化的体现 +- 往往伴随更强的支撑/压力作用 +- 破位后影响更大 + +### 技巧4:多空力量对比 +查看标签中的触碰次数: +- 支撑触碰多 → 买盘强劲 +- 压力触碰多 → 卖压较重 +- 对比两者评估多空力量对比 + +### 技巧5:ATR 容差优化 +根据市场波动调整容差: +- 高波动市场(加密货币):增大到 0.4-0.5 +- 低波动市场(外汇主要货币对):减小到 0.2-0.3 +- 确保水平线不过密也不过疏 + +## 📞 常见问题 + +**Q: 为什么有时候强度很高的水平没显示?** +A: 可能超出了回溯周期,或者不在最近的 maxVisibleLevels 个水平中。可以增大这些参数。 + +**Q: 突破标记后价格又回来了,怎么办?** +A: 这就是假突破,属于正常现象。等待回测确认是过滤假突破的关键。 + +**Q: 强度分数一直在变化正常吗?** +A: 正常。强度包含时效性,随时间推移会衰减。新的触碰会提升强度。 + +**Q: 区域模式和线条模式哪个好?** +A: 区域模式更符合实际,但线条模式更清晰。建议根据个人习惯选择。 + +**Q: 如何判断突破是否有效?** +A: 看三点:1) 收盘价突破 2) 成交量确认 3) 回测不破。三者都满足成功率较高。 + +--- + +**版本**:v1.0 优化版 +**更新日期**:2025-10-21 +**适用于**:TradingView Pine Script v6 + diff --git a/短期支撑压力位.pine b/短期支撑压力位.pine new file mode 100644 index 0000000..21422a5 --- /dev/null +++ b/短期支撑压力位.pine @@ -0,0 +1,268 @@ +//@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) + + diff --git a/趋势行情判断.pine b/趋势行情判断.pine new file mode 100644 index 0000000..121b988 --- /dev/null +++ b/趋势行情判断.pine @@ -0,0 +1,150 @@ +//@version=6 +indicator(title="趋势/震荡行情判定 (BTC 1H)", shorttitle="Trend/Range Regime", overlay=true, max_labels_count=500) + +// ========================= 输入参数 ========================= +// 基础 +symbolTitle = input.string(defval="BTCUSD/USDT 1H", title="使用说明 (默认为 1 小时 BTC)", inline="hdr") +showEma = input.bool(true, "显示EMA", inline="ema") +emaLen = input.int(52, "长度", minval=1, inline="ema") + +// 趋势强度/震荡指标参数 +adxLen = input.int(14, "ADX长度", minval=2, inline="adx") +adxTrendThresh = input.float(22.0, "趋势阈值", minval=5, inline="adx") +adxRangeThresh = input.float(18.0, "震荡阈值", minval=5, inline="adx") + +chopLen = input.int(14, "CHOP长度", minval=2, inline="chop") +chopTrendMax = input.float(45.0, "趋势上限(越小越趋势)", minval=10, maxval=100, inline="chop") +chopRangeMin = input.float(55.0, "震荡下限(越大越震荡)", minval=10, maxval=100, inline="chop") + +// 方向与确认 +slopePctThresh = input.float(0.05, "EMA斜率阈值(%/bar)", minval=0.0, step=0.01) +distancePctThresh = input.float(0.10, "价格偏离EMA阈值(%)", minval=0.0, step=0.01) +enterConfirmBars = input.int(2, "进入确认根数", minval=1) +exitConfirmBars = input.int(2, "退出确认根数", minval=1) + +// 灵敏度:>1 更灵敏,<1 更稳健 +sensitivity = input.float(1.2, "灵敏度(>1更灵敏,<1更稳健)", minval=0.5, maxval=2.0, step=0.05) + +// 可视化与告警 +showBackground = input.bool(true, "背景着色") +showLabels = input.bool(true, "标记切换点") + +// 颜色 +colUp = color.new(color.teal, 80) +colDn = color.new(color.red, 80) +colRg = color.new(color.gray, 85) + +// ========================= 指标计算 ========================= +ema = ta.ema(close, emaLen) +plot(showEma ? ema : na, color=color.new(color.yellow, 0), linewidth=2, title="EMA") + +// EMA 斜率(百分比/每根) +emaSlopePct = ema != 0.0 and ema[1] != 0.0 ? 100.0 * (ema - ema[1]) / ema[1] : 0.0 +priceDistPct = ema != 0.0 ? 100.0 * (close - ema) / ema : 0.0 + +// ADX(手动实现,避免环境不支持 ta.adx) +upMove = ta.change(high) +downMove = -ta.change(low) +plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0 +minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0 +trAdx = ta.tr(true) +plusDI = 100.0 * ta.rma(plusDM, adxLen) / ta.rma(trAdx, adxLen) +minusDI = 100.0 * ta.rma(minusDM, adxLen) / ta.rma(trAdx, adxLen) +dx = (plusDI + minusDI > 0) ? 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI) : 0.0 +adx = ta.rma(dx, adxLen) + +// CHOP (Choppiness Index) +var float log10 = math.log(10.0) +tr = ta.tr(true) +sumTr = ta.sma(tr, chopLen) * chopLen +hh = ta.highest(high, chopLen) +ll = ta.lowest(low, chopLen) +rangeHL = math.max(hh - ll, 1e-10) +chop = 100.0 * (math.log(sumTr / rangeHL) / log10) / (math.log(chopLen) / log10) + +// ========================= 阈值动态调整(按灵敏度) ========================= +sens = sensitivity +adxTrendThreshAdj = adxTrendThresh / sens +adxRangeThreshAdj = adxRangeThresh * sens +chopTrendMaxAdj = math.min(100.0, chopTrendMax * sens) +chopRangeMinAdj = math.max(10.0, chopRangeMin / sens) +slopePctThreshAdj = slopePctThresh / sens +distancePctThreshAdj = distancePctThresh / sens +enterConfirmAdj = math.max(1, int(math.round(enterConfirmBars / sens))) +exitConfirmAdj = math.max(1, int(math.round(exitConfirmBars / sens))) + +// 条件 +isTrendStrength = adx > adxTrendThreshAdj and chop < chopTrendMaxAdj +isRangeWeak = adx < adxRangeThreshAdj or chop > chopRangeMinAdj + +dirUpRaw = emaSlopePct > slopePctThreshAdj and priceDistPct > distancePctThreshAdj +dirDnRaw = emaSlopePct < -slopePctThreshAdj and priceDistPct < -distancePctThreshAdj + +upReady = isTrendStrength and dirUpRaw +dnReady = isTrendStrength and dirDnRaw + +// 连续确认 +upConsec = ta.barssince(not upReady) +dnConsec = ta.barssince(not dnReady) +upConfirmed = upConsec >= enterConfirmAdj - 1 +dnConfirmed = dnConsec >= enterConfirmAdj - 1 + +// 退出确认(从趋势转入震荡或反向) +exitWeak = isRangeWeak or (not isTrendStrength) +exitUpConsec = ta.barssince(not exitWeak) >= exitConfirmAdj - 1 +exitDnConsec = ta.barssince(not exitWeak) >= exitConfirmAdj - 1 + +// ========================= 状态机 ========================= +// 0: 震荡, 1: 上涨趋势, -1: 下跌趋势 +var int regime = 0 +prevRegime = nz(regime[1], 0) + +// 选择方向时的冲突消解:优先单边确认,避免同根双向 +chooseUp = upConfirmed and not dnConfirmed +chooseDn = dnConfirmed and not upConfirmed + +// 状态更新(仅在收盘确认时变更) +calcRegime = prevRegime == 0 ? (chooseUp ? 1 : (chooseDn ? -1 : 0)) : + prevRegime == 1 ? ((exitWeak and not upReady) ? 0 : ((dnConfirmed and isTrendStrength) ? -1 : 1)) : + ((exitWeak and not dnReady) ? 0 : ((upConfirmed and isTrendStrength) ? 1 : -1)) + +regime := barstate.isconfirmed ? calcRegime : nz(regime[1], prevRegime) + +// ========================= 可视化 ========================= +bgcolor(showBackground ? (regime == 1 ? colUp : regime == -1 ? colDn : colRg) : na, title="背景") + +// 切换点标记 +enteredUp = barstate.isconfirmed and regime == 1 and prevRegime != 1 +enteredDn = barstate.isconfirmed and regime == -1 and prevRegime != -1 +enteredRg = barstate.isconfirmed and regime == 0 and prevRegime != 0 + +// 离开点(结束点) +leftUp = barstate.isconfirmed and prevRegime == 1 and regime != 1 +leftDn = barstate.isconfirmed and prevRegime == -1 and regime != -1 +leftRg = barstate.isconfirmed and prevRegime == 0 and regime != 0 + +plotchar(showLabels and enteredUp, title="↑ Up开始", char="↑", location=location.belowbar, color=color.new(color.teal, 0), size=size.tiny) +plotchar(showLabels and leftUp, title="↓ Up结束", char="↓", location=location.abovebar, color=color.new(color.gray, 0), size=size.tiny) +plotchar(showLabels and enteredDn, title="↓ Down开始", char="↓", location=location.abovebar, color=color.new(color.red, 0), size=size.tiny) +plotchar(showLabels and leftDn, title="↑ Down结束", char="↑", location=location.belowbar, color=color.new(color.gray, 0), size=size.tiny) +plotchar(showLabels and enteredRg, title="≈ Range开始",char="≈", location=location.top, color=color.new(color.silver, 0), size=size.tiny) +plotchar(showLabels and leftRg, title="≠ Range结束",char="≠", location=location.top, color=color.new(color.silver, 0), size=size.tiny) + +// 辅助输出 +plotchar(regime == 1, title="UpTrend", char="U", location=location.top, color=color.teal, size=size.tiny) +plotchar(regime == -1, title="DownTrend", char="D", location=location.top, color=color.red, size=size.tiny) +plotchar(regime == 0, title="Range", char="R", location=location.top, color=color.gray, size=size.tiny) + +// ========================= 告警 ========================= +alertcondition(enteredUp, title="上涨趋势开始", message="上涨趋势开始 (BTC 1H)") +alertcondition(leftUp, title="上涨趋势结束", message="上涨趋势结束 (BTC 1H)") +alertcondition(enteredDn, title="下跌趋势开始", message="下跌趋势开始 (BTC 1H)") +alertcondition(leftDn, title="下跌趋势结束", message="下跌趋势结束 (BTC 1H)") +alertcondition(enteredRg, title="震荡开始", message="震荡开始 (BTC 1H)") +alertcondition(leftRg, title="震荡结束", message="震荡结束 (BTC 1H)") + +// ========================= 说明 ========================= +// 建议用于 1 小时 BTC;参数已做温和默认值以区分趋势/震荡。 +// 如需更敏感:降低 adxTrendThresh、chopTrendMax,降低 slopePctThresh/confirm;反之亦然。 + +