//@version=6
strategy(
"Volume Profile 买卖策略 v6",
overlay = true,
max_bars_back = 2000,
max_boxes_count = 300,
max_labels_count = 200,
pyramiding = 0,
process_orders_on_close = true,
calc_on_order_fills = true,
commission_type = strategy.commission.percent,
commission_value = 0.10,
slippage = 1,
initial_capital = 10000,
default_qty_type = strategy.fixed,
default_qty_value = 1)
// =============================================================================
// 重要说明
// 1) 这是滚动范围 Volume Profile:每根 K 线都使用“前 N 根已完成 K 线”计算,
// 当前信号 K 线不会被放进 Profile,尽量避免前视偏差。
// 2) 成交量按每根 K 线的 HLC3 归入一个价格行,是 OHLCV 近似,不等于逐笔成交。
// 3) Up / Down Volume 按 K 线涨跌分类,不是真实 Bid / Ask Delta。
// 4) 策略用于研究和回测,不构成投资建议;实盘前请自行做样本外测试。
// =============================================================================
// ── Profile 设置
string G_PROFILE = "1. Volume Profile"
int lookbackBars = input.int(120, "滚动范围 K 线数", minval = 20, maxval = 500, group = G_PROFILE)
int rows = input.int(36, "价格行数", minval = 12, maxval = 100, group = G_PROFILE)
float valueAreaPct = input.float(70.0, "价值区成交量 %", minval = 50, maxval = 95, step = 1, group = G_PROFILE) / 100.0
bool showProfile = input.bool(true, "显示右侧 Profile", group = G_PROFILE)
int profileOffset = input.int(5, "右侧偏移", minval = 1, maxval = 100, group = G_PROFILE)
int profileWidth = input.int(40, "最大宽度(K 线)", minval = 10, maxval = 100, group = G_PROFILE)
color upColor = input.color(color.rgb(0, 188, 212), "上涨量颜色", group = G_PROFILE)
color downColor = input.color(color.rgb(233, 30, 99), "下跌量颜色", group = G_PROFILE)
int profileTransparency = input.int(25, "透明度", minval = 0, maxval = 90, group = G_PROFILE)
// ── 信号设置
string G_SIGNAL = "2. 买卖信号"
string signalMode = input.string("VAH/VAL 突破", "信号模式",
options = ["VAH/VAL 突破", "边界假突破反转", "POC 重夺/跌破", "自动组合"], group = G_SIGNAL)
string tradeMode = input.string("现货:只做多", "交易模式",
options = ["现货:只做多", "合约:多空双向"], group = G_SIGNAL)
string oppositeAction = input.string("直接反手", "合约相反信号",
options = ["直接反手", "只平仓"], group = G_SIGNAL)
int cooldownBars = input.int(2, "信号冷却 K 线", minval = 0, maxval = 50, group = G_SIGNAL)
bool confirmCandle = input.bool(true, "要求信号 K 线方向一致", group = G_SIGNAL)
// ── 过滤器
string G_FILTER = "3. 过滤器"
bool useEma = input.bool(true, "启用 EMA 趋势过滤", group = G_FILTER)
int fastEmaLen = input.int(8, "快速 EMA", minval = 1, group = G_FILTER)
int slowEmaLen = input.int(21, "慢速 EMA", minval = 2, group = G_FILTER)
bool useVolume = input.bool(true, "启用相对成交量过滤", group = G_FILTER)
int volumeLen = input.int(20, "成交量均线周期", minval = 2, group = G_FILTER)
float minRelVolume = input.float(0.9, "最低相对成交量倍数", minval = 0, step = 0.1, group = G_FILTER)
bool useRsi = input.bool(false, "启用 RSI 过滤", group = G_FILTER)
int rsiLen = input.int(14, "RSI 周期", minval = 2, group = G_FILTER)
float rsiLongMin = input.float(50, "做多 RSI 下限", minval = 0, maxval = 100, group = G_FILTER)
float rsiShortMax = input.float(50, "做空 RSI 上限", minval = 0, maxval = 100, group = G_FILTER)
// ── 风险控制
string G_RISK = "4. 风险控制"
float riskPct = input.float(1.0, "每笔风险占权益 %", minval = 0.1, maxval = 10, step = 0.1, group = G_RISK)
float maxExposurePct = input.float(100, "最大名义仓位占权益 %", minval = 1, maxval = 500, step = 5, group = G_RISK)
int atrLen = input.int(14, "ATR 周期", minval = 1, group = G_RISK)
float atrStopMult = input.float(1.8, "初始止损 ATR 倍数", minval = 0.1, step = 0.1, group = G_RISK)
float rewardRisk = input.float(2.0, "止盈风险回报比", minval = 0.2, step = 0.1, group = G_RISK)
bool useBreakeven = input.bool(true, "启用保本止损", group = G_RISK)
float breakevenAtR = input.float(1.0, "盈利达到 R 后保本", minval = 0.2, step = 0.1, group = G_RISK)
bool useTrail = input.bool(false, "启用 ATR 移动止损", group = G_RISK)
float trailAtrMult = input.float(2.0, "移动止损 ATR 倍数", minval = 0.2, step = 0.1, group = G_RISK)
// ── 显示与警报
string G_DISPLAY = "5. 显示与警报"
bool showEma = input.bool(true, "显示 EMA", group = G_DISPLAY)
bool showLabels = input.bool(true, "显示买卖标签", group = G_DISPLAY)
bool showPanel = input.bool(true, "显示状态面板", group = G_DISPLAY)
string alertFormat = input.string("中文文本", "alert() 格式",
options = ["中文文本", "Webhook JSON"], group = G_DISPLAY)
// ── 基础指标
float emaFast = ta.ema(close, fastEmaLen)
float emaSlow = ta.ema(close, slowEmaLen)
float relVolume = volume / math.max(ta.sma(volume, volumeLen), 1.0)
float rsiValue = ta.rsi(close, rsiLen)
float atrValue = ta.atr(atrLen)
bool enoughBars = bar_index > lookbackBars + 2
// ── 每根 K 线重算前 N 根已完成 K 线的 Volume Profile
var array<float> totalRows = array.new_float(rows, 0.0)
var array<float> upRows = array.new_float(rows, 0.0)
var array<float> downRows = array.new_float(rows, 0.0)
array.fill(totalRows, 0.0)
array.fill(upRows, 0.0)
array.fill(downRows, 0.0)
float rollingHigh = ta.highest(high[1], lookbackBars)
float rollingLow = ta.lowest(low[1], lookbackBars)
float rangeHigh = enoughBars ? rollingHigh : na
float rangeLow = enoughBars ? rollingLow : na
float rowSize = enoughBars ? math.max((rangeHigh - rangeLow) / rows, syminfo.mintick) : na
float totalVolume = 0.0
if enoughBars
for i = 1 to lookbackBars
float priceForBin = hlc3[i]
int rawBin = int(math.floor((priceForBin - rangeLow) / rowSize))
int bin = math.max(0, math.min(rows - 1, rawBin))
float barVolume = nz(volume[i])
array.set(totalRows, bin, array.get(totalRows, bin) + barVolume)
if close[i] >= open[i]
array.set(upRows, bin, array.get(upRows, bin) + barVolume)
else
array.set(downRows, bin, array.get(downRows, bin) + barVolume)
totalVolume += barVolume
// ── POC 与 Value Area
float maxRowVolume = 0.0
int pocIndex = 0
if enoughBars
for i = 0 to rows - 1
float rowVolume = array.get(totalRows, i)
if rowVolume > maxRowVolume
maxRowVolume := rowVolume
pocIndex := i
int vaLowIndex = pocIndex
int vaHighIndex = pocIndex
float vaVolume = enoughBars ? array.get(totalRows, pocIndex) : 0.0
float vaTarget = totalVolume * valueAreaPct
if enoughBars
int safety = 0
while vaVolume < vaTarget and (vaLowIndex > 0 or vaHighIndex < rows - 1) and safety < rows
float belowVolume = vaLowIndex > 0 ? array.get(totalRows, vaLowIndex - 1) : -1.0
float aboveVolume = vaHighIndex < rows - 1 ? array.get(totalRows, vaHighIndex + 1) : -1.0
if aboveVolume >= belowVolume
vaHighIndex += 1
vaVolume += math.max(aboveVolume, 0.0)
else
vaLowIndex -= 1
vaVolume += math.max(belowVolume, 0.0)
safety += 1
float poc = enoughBars ? rangeLow + (pocIndex + 0.5) * rowSize : na
float vah = enoughBars ? rangeLow + (vaHighIndex + 1.0) * rowSize : na
float val = enoughBars ? rangeLow + vaLowIndex * rowSize : na
// ── 原始信号:Profile 不包含当前 K 线
bool breakLong = enoughBars and close > vah and close[1] <= vah[1]
bool breakShort = enoughBars and close < val and close[1] >= val[1]
bool rejectLong = enoughBars and low < val and close > val
bool rejectShort = enoughBars and high > vah and close < vah
bool pocLong = enoughBars and close > poc and close[1] <= poc[1]
bool pocShort = enoughBars and close < poc and close[1] >= poc[1]
bool rawLong = switch signalMode
"VAH/VAL 突破" => breakLong
"边界假突破反转" => rejectLong
"POC 重夺/跌破" => pocLong
=> breakLong or rejectLong
bool rawShort = switch signalMode
"VAH/VAL 突破" => breakShort
"边界假突破反转" => rejectShort
"POC 重夺/跌破" => pocShort
=> breakShort or rejectShort
bool emaLongOk = not useEma or emaFast > emaSlow
bool emaShortOk = not useEma or emaFast < emaSlow
bool volumeOk = not useVolume or relVolume >= minRelVolume
bool rsiLongOk = not useRsi or rsiValue >= rsiLongMin
bool rsiShortOk = not useRsi or rsiValue <= rsiShortMax
bool candleLongOk = not confirmCandle or close > open
bool candleShortOk = not confirmCandle or close < open
var int lastSignalBar = na
bool cooldownOk = na(lastSignalBar) or bar_index - lastSignalBar > cooldownBars
bool longSignal = barstate.isconfirmed and rawLong and emaLongOk and volumeOk and rsiLongOk and candleLongOk and cooldownOk
bool shortSignal = barstate.isconfirmed and rawShort and emaShortOk and volumeOk and rsiShortOk and candleShortOk and cooldownOk
// ── 风险仓位:同时受“每笔风险”和“最大名义仓位”限制
float stopDistance = math.max(atrValue * atrStopMult, syminfo.mintick)
float riskCash = strategy.equity * riskPct / 100.0
float riskQty = riskCash / stopDistance
float exposureQty = strategy.equity * maxExposurePct / 100.0 / close
float orderQty = math.max(0.0, math.min(riskQty, exposureQty))
// ── 持仓状态与止损
var float activeRisk = na
var float activeStop = na
var float activeTarget = na
string buyText = "买进 " + syminfo.ticker + " @ " + str.tostring(close, format.mintick)
string sellText = "卖出/做空 " + syminfo.ticker + " @ " + str.tostring(close, format.mintick)
string buyJson = "{\"action\":\"BUY\",\"symbol\":\"" + syminfo.tickerid + "\",\"timeframe\":\"" + timeframe.period + "\",\"price\":\"" + str.tostring(close, format.mintick) + "\",\"poc\":\"" + str.tostring(poc, format.mintick) + "\",\"vah\":\"" + str.tostring(vah, format.mintick) + "\",\"val\":\"" + str.tostring(val, format.mintick) + "\"}"
string sellJson = "{\"action\":\"SELL\",\"symbol\":\"" + syminfo.tickerid + "\",\"timeframe\":\"" + timeframe.period + "\",\"price\":\"" + str.tostring(close, format.mintick) + "\",\"poc\":\"" + str.tostring(poc, format.mintick) + "\",\"vah\":\"" + str.tostring(vah, format.mintick) + "\",\"val\":\"" + str.tostring(val, format.mintick) + "\"}"
string longAlert = alertFormat == "Webhook JSON" ? buyJson : buyText
string shortAlert = alertFormat == "Webhook JSON" ? sellJson : sellText
if longSignal
lastSignalBar := bar_index
if tradeMode == "现货:只做多"
if strategy.position_size <= 0
activeRisk := stopDistance
activeStop := close - activeRisk
activeTarget := close + activeRisk * rewardRisk
strategy.entry("Long", strategy.long, qty = orderQty, alert_message = longAlert)
alert(longAlert, alert.freq_once_per_bar_close)
else
if strategy.position_size < 0 and oppositeAction == "只平仓"
strategy.close("Short", comment = "平空", alert_message = longAlert)
else
activeRisk := stopDistance
activeStop := close - activeRisk
activeTarget := close + activeRisk * rewardRisk
strategy.entry("Long", strategy.long, qty = orderQty, alert_message = longAlert)
alert(longAlert, alert.freq_once_per_bar_close)
if shortSignal
lastSignalBar := bar_index
if tradeMode == "现货:只做多"
if strategy.position_size > 0
strategy.close("Long", comment = "卖出", alert_message = shortAlert)
alert(shortAlert, alert.freq_once_per_bar_close)
else
if strategy.position_size > 0 and oppositeAction == "只平仓"
strategy.close("Long", comment = "平多", alert_message = shortAlert)
else
activeRisk := stopDistance
activeStop := close + activeRisk
activeTarget := close - activeRisk * rewardRisk
strategy.entry("Short", strategy.short, qty = orderQty, alert_message = shortAlert)
alert(shortAlert, alert.freq_once_per_bar_close)
// 持仓后动态管理止损;保本和移动止损只会收紧,不会放宽。
if strategy.position_size > 0 and not na(activeRisk)
float entryPrice = strategy.position_avg_price
if useBreakeven and high >= entryPrice + activeRisk * breakevenAtR
activeStop := math.max(activeStop, entryPrice)
if useTrail
activeStop := math.max(activeStop, close - atrValue * trailAtrMult)
activeTarget := entryPrice + activeRisk * rewardRisk
strategy.exit("Long Exit", from_entry = "Long", stop = activeStop, limit = activeTarget,
alert_message = "平多 " + syminfo.ticker)
if strategy.position_size < 0 and not na(activeRisk)
float entryPrice = strategy.position_avg_price
if useBreakeven and low <= entryPrice - activeRisk * breakevenAtR
activeStop := math.min(activeStop, entryPrice)
if useTrail
activeStop := math.min(activeStop, close + atrValue * trailAtrMult)
activeTarget := entryPrice - activeRisk * rewardRisk
strategy.exit("Short Exit", from_entry = "Short", stop = activeStop, limit = activeTarget,
alert_message = "平空 " + syminfo.ticker)
if strategy.position_size == 0 and strategy.position_size[1] != 0
activeRisk := na
activeStop := na
activeTarget := na
// ── 图表显示
plot(poc, "POC", color = color.orange, linewidth = 2)
plot(vah, "VAH", color = color.aqua, linewidth = 1)
plot(val, "VAL", color = color.aqua, linewidth = 1)
plot(showEma ? emaFast : na, "快速 EMA", color = color.yellow)
plot(showEma ? emaSlow : na, "慢速 EMA", color = color.blue)
plot(strategy.position_size != 0 ? activeStop : na, "动态止损", color = color.red, style = plot.style_linebr)
plot(strategy.position_size != 0 ? activeTarget : na, "止盈", color = color.lime, style = plot.style_linebr)
plotshape(showLabels and longSignal, title = "买进", text = "买进", style = shape.labelup,
location = location.belowbar, color = color.new(color.lime, 0), textcolor = color.black, size = size.tiny)
plotshape(showLabels and shortSignal and tradeMode == "现货:只做多", title = "卖出", text = "卖出",
style = shape.labeldown, location = location.abovebar, color = color.new(color.red, 0), textcolor = color.white, size = size.tiny)
plotshape(showLabels and shortSignal and tradeMode == "合约:多空双向", title = "做空", text = "做空",
style = shape.labeldown, location = location.abovebar, color = color.new(color.red, 0), textcolor = color.white, size = size.tiny)
// 只在最右侧画 Profile,避免每根历史 K 线产生大量 box。
var array<box> profileBoxes = array.new_box()
if barstate.islast
while array.size(profileBoxes) > 0
box.delete(array.pop(profileBoxes))
if showProfile and enoughBars and maxRowVolume > 0
int profileLeft = bar_index + profileOffset
for i = 0 to rows - 1
float rowTotal = array.get(totalRows, i)
float rowUp = array.get(upRows, i)
float rowDown = array.get(downRows, i)
int totalBarWidth = int(math.round(profileWidth * rowTotal / maxRowVolume))
int upBarWidth = rowTotal > 0 ? int(math.round(totalBarWidth * rowUp / rowTotal)) : 0
int downBarWidth = rowTotal > 0 ? int(math.round(totalBarWidth * rowDown / rowTotal)) : 0
float bottomPrice = rangeLow + i * rowSize
float topPrice = bottomPrice + rowSize
bool inValueArea = i >= vaLowIndex and i <= vaHighIndex
int extraTransparency = inValueArea ? 0 : 35
if upBarWidth > 0
box upBox = box.new(left = profileLeft, top = topPrice, right = profileLeft + upBarWidth,
bottom = bottomPrice, xloc = xloc.bar_index,
bgcolor = color.new(upColor, math.min(95, profileTransparency + extraTransparency)),
border_color = color.new(upColor, 100))
array.push(profileBoxes, upBox)
if downBarWidth > 0
box downBox = box.new(left = profileLeft + upBarWidth, top = topPrice,
right = profileLeft + upBarWidth + downBarWidth, bottom = bottomPrice,
xloc = xloc.bar_index,
bgcolor = color.new(downColor, math.min(95, profileTransparency + extraTransparency)),
border_color = color.new(downColor, 100))
array.push(profileBoxes, downBox)
// ── 状态面板
var table panel = table.new(position.top_right, 2, 9, border_width = 1)
if barstate.islast
if showPanel
string priceZone = close > vah ? "价值区上方" : close < val ? "价值区下方" : "价值区内部"
string positionText = strategy.position_size > 0 ? "多单" : strategy.position_size < 0 ? "空单" : "空仓"
table.cell(panel, 0, 0, "Volume Profile 策略", text_color = color.white, bgcolor = color.rgb(40, 40, 40))
table.cell(panel, 1, 0, signalMode, text_color = color.white, bgcolor = color.rgb(40, 40, 40))
table.cell(panel, 0, 1, "POC")
table.cell(panel, 1, 1, str.tostring(poc, format.mintick))
table.cell(panel, 0, 2, "VAH")
table.cell(panel, 1, 2, str.tostring(vah, format.mintick))
table.cell(panel, 0, 3, "VAL")
table.cell(panel, 1, 3, str.tostring(val, format.mintick))
table.cell(panel, 0, 4, "现价位置")
table.cell(panel, 1, 4, priceZone)
table.cell(panel, 0, 5, "相对成交量")
table.cell(panel, 1, 5, str.tostring(relVolume, "#.##") + "x")
table.cell(panel, 0, 6, "RSI")
table.cell(panel, 1, 6, str.tostring(rsiValue, "#.0"))
table.cell(panel, 0, 7, "持仓")
table.cell(panel, 1, 7, positionText)
table.cell(panel, 0, 8, "提醒")
table.cell(panel, 1, 8, "回测后再使用", text_color = color.yellow)
else
table.clear(panel, 0, 0, 1, 8)