添加新的支撑和压力位显示

This commit is contained in:
jackyu66git
2025-10-21 17:32:33 +08:00
parent cdc36aab9e
commit 415a2695ff
5 changed files with 1483 additions and 0 deletions
+325
View File
@@ -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)
+524
View File
@@ -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)
+216
View File
@@ -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. 止损设在突破前的高/低点
### 场景3S/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:多空力量对比
查看标签中的触碰次数:
- 支撑触碰多 → 买盘强劲
- 压力触碰多 → 卖压较重
- 对比两者评估多空力量对比
### 技巧5ATR 容差优化
根据市场波动调整容差:
- 高波动市场(加密货币):增大到 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
+268
View File
@@ -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)
+150
View File
@@ -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;反之亦然。