269 lines
12 KiB
Plaintext
269 lines
12 KiB
Plaintext
//@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)
|
|
|
|
|