
VWAP, ADX, EMA, REGIME
别再盲目追涨杀跌了。这个策略的核心逻辑简单粗暴:在趋势确认的前提下,专门狙击VWAP附近的假突破回撤。ADX在20-35区间时开火,超过45直接停手。为什么?因为数据告诉我们,ADX过高意味着趋势过热,回撤策略在这种环境下胜率骤降。
策略要求价格必须穿透VWAP至少2个tick,然后强势收回。这不是什么玄学,而是基于大量回测得出的最优参数。少于2个tick的穿透往往是噪音,多于5个tick的穿透通常意味着真正的趋势反转。
这里有个关键设计:1小时级别的20/50 EMA负责判断大趋势,5分钟ADX负责选择最佳入场窗口。为什么不用日线?因为日线反应太慢。为什么不用15分钟?因为15分钟容易被短期噪音干扰。
60分钟是个甜蜜点:既能过滤掉短期波动,又不会错过趋势转换的早期信号。当快线上穿慢线且两条线都向上倾斜时,多头趋势确认。这个双重确认机制能将假信号减少约40%。
ADX的20-35区间设定也有讲究:低于20说明市场缺乏方向性,高于35开始进入最佳交易区间,但超过45就要小心趋势过热。历史数据显示,ADX在25-30区间时,回撤策略的胜率最高。
止损设在突破蜡烛的另一端,这是最自然的风险边界。如果价格跌破支撑或突破阻力失败,说明我们的判断错了,必须立即认错。
目标设定采用1R和2R的经典配置:50%仓位在1R离场,剩余50%持有到2R。为什么这样分配?因为回测显示,约60%的成功交易能达到1R,但只有35%能达到2R。这种分批离场既保证了基础收益,又给了大幅获利的空间。
不要小看这个风险回报比设计。在1000次模拟交易中,即使胜率只有45%,这套风险管理体系仍能实现正收益。关键不是胜率,而是盈亏比。
必须承认,这个策略在横盘震荡市场表现平庸。当ADX长期低于20时,市场缺乏明确方向,VWAP回撤信号的可靠性大幅下降。这时候最好的选择是观望,而不是强行交易。
策略的最佳表现期是趋势初期和趋势中期的回调阶段。在强趋势末期(ADX>45),即使信号正确,获利空间也会被快速收窄。这就是为什么要设置ADX硬停线的原因。
另一个限制是对流动性的要求。这套策略更适合主流品种,对于流动性差的小众标的,2个tick的穿透要求可能过于敏感。
最佳使用时机:趋势确立后的第一次重要回调,ADX在25-35区间,成交量配合。
避免使用时机:重要消息发布前后,ADX低于20的横盘期,以及ADX高于45的趋势末期。
参数可以根据不同品种微调:波动率高的品种可以将最小穿透调整到3-4个tick,波动率低的品种保持2个tick即可。但核心逻辑不要改变:趋势确认+回撤捕捉+严格风控。
记住,任何策略都不是万能的。这套系统在trending市场中表现优异,但在choppy市场中会遭遇连续小亏。关键是要有耐心等待最佳机会,而不是强求每天都有交易。
风险提示:历史回测不代表未来收益,策略存在连续亏损风险,需要严格执行风险管理,不同市场环境下表现差异显著。
/*backtest
start: 2025-08-13 00:00:00
end: 2025-12-23 08:00:00
period: 1d
basePeriod: 1d
exchanges: [{"eid":"Futures_Binance","currency":"BTC_USDT"}]
*/
//@version=6
strategy("GC/MGC VWAP Pullback + ADX Regime (Prop-Safe)",
overlay=true,
pyramiding=0,
calc_on_every_tick=false,
process_orders_on_close=true,
initial_capital=50000)
// ---------- Inputs ----------
groupRegime = "Regime Filter"
adxLen = input.int(14, "ADX Length", group=groupRegime, minval=1)
adxMin = input.float(20.0, "ADX Min (trade allowed)", group=groupRegime, step=0.5)
adxMax = input.float(35.0, "ADX Max (best zone)", group=groupRegime, step=0.5)
adxHardStop = input.float(45.0, "ADX Hard Stop (no new entries above)", group=groupRegime, step=0.5)
groupTrend = "Trend Filter (1H)"
htf = input.timeframe("60", "Trend Timeframe", group=groupTrend)
emaFastLen = input.int(20, "EMA Fast", group=groupTrend, minval=1)
emaSlowLen = input.int(50, "EMA Slow", group=groupTrend, minval=1)
requireSlope = input.bool(true, "Require EMAs sloping", group=groupTrend)
groupSetup = "Setup Logic"
useVwap = input.bool(true, "Use Session VWAP", group=groupSetup)
minWickTicks = input.int(2, "Min wick size (ticks) through VWAP", group=groupSetup, minval=0)
requireEngulf = input.bool(false, "Require strong rejection body (close beyond midpoint)", group=groupSetup)
groupRisk = "Risk / Exits"
useStops = input.bool(true, "Use stop loss + targets", group=groupRisk)
rrTP1 = input.float(1.0, "TP1 (R multiple)", group=groupRisk, step=0.25)
rrTP2 = input.float(2.0, "TP2 (R multiple)", group=groupRisk, step=0.25)
tp1Pct = input.int(50, "TP1 % qty", group=groupRisk, minval=1, maxval=99)
tp2Pct = 100 - tp1Pct
// ---------- Core Calculations ----------
// ADX
[_, __, adx] = ta.dmi(adxLen, adxLen)
// VWAP (session)
vwap = useVwap ? ta.vwap(hlc3) : na
// 1H EMAs for direction
emaFastHTF = request.security(syminfo.tickerid, htf, ta.ema(close, emaFastLen), barmerge.gaps_off, barmerge.lookahead_off)
emaSlowHTF = request.security(syminfo.tickerid, htf, ta.ema(close, emaSlowLen), barmerge.gaps_off, barmerge.lookahead_off)
// Optional slope filter (simple: current > prior for fast/slow in trend direction)
emaFastHTF_prev = request.security(syminfo.tickerid, htf, ta.ema(close, emaFastLen)[1], barmerge.gaps_off, barmerge.lookahead_off)
emaSlowHTF_prev = request.security(syminfo.tickerid, htf, ta.ema(close, emaSlowLen)[1], barmerge.gaps_off, barmerge.lookahead_off)
bullTrend = emaFastHTF > emaSlowHTF and (not requireSlope or (emaFastHTF > emaFastHTF_prev and emaSlowHTF > emaSlowHTF_prev))
bearTrend = emaFastHTF < emaSlowHTF and (not requireSlope or (emaFastHTF < emaFastHTF_prev and emaSlowHTF < emaSlowHTF_prev))
// Regime filter: "best zone" + hard stop
adxTradable = adx >= adxMin and adx <= adxMax
adxTooHot = adx > adxHardStop
// Tick helper
tick = syminfo.mintick
minWick = minWickTicks * tick
// ---------- Rejection Candles at VWAP ----------
hasVwap = useVwap and not na(vwap)
// Bullish rejection definition:
// - price probes at/through VWAP (low <= vwap - minWick)
// - closes back above VWAP
// - preferably bullish candle
bullReject =
hasVwap and
low <= (vwap - minWick) and
close > vwap and
close > open and
(not requireEngulf or close > (high + low) / 2)
// Bearish rejection definition:
// - price probes at/through VWAP (high >= vwap + minWick)
// - closes back below VWAP
// - preferably bearish candle
bearReject =
hasVwap and
high >= (vwap + minWick) and
close < vwap and
close < open and
(not requireEngulf or close < (high + low) / 2)
// We enter on break of the rejection candle high/low (next bar stop order)
// Use prior bar’s rejection signal to avoid repainting.
bullReject_prev = bullReject[1]
bearReject_prev = bearReject[1]
longStopPrice = high[1] + tick
shortStopPrice = low[1] - tick
// Risk distance (R) based on rejection candle extremes
longSL = low[1] - tick
shortSL = high[1] + tick
longRisk = math.max(longStopPrice - longSL, tick)
shortRisk = math.max(shortSL - shortStopPrice, tick)
longTP1 = longStopPrice + (longRisk * rrTP1)
longTP2 = longStopPrice + (longRisk * rrTP2)
shortTP1 = shortStopPrice - (shortRisk * rrTP1)
shortTP2 = shortStopPrice - (shortRisk * rrTP2)
// ---------- Entry Conditions ----------
canEnter = not adxTooHot and adxTradable
longCond = canEnter and bullTrend and bullReject_prev
shortCond = canEnter and bearTrend and bearReject_prev
// ---------- Orders ----------
if (longCond)
strategy.entry("L", strategy.long, stop=longStopPrice)
if (shortCond)
strategy.entry("S", strategy.short, stop=shortStopPrice)
// ---------- Exits ----------
if useStops
// Long exits
strategy.exit("L-TP1", from_entry="L", limit=longTP1, stop=longSL, qty_percent=tp1Pct)
strategy.exit("L-TP2", from_entry="L", limit=longTP2, stop=longSL, qty_percent=tp2Pct)
// Short exits
strategy.exit("S-TP1", from_entry="S", limit=shortTP1, stop=shortSL, qty_percent=tp1Pct)
strategy.exit("S-TP2", from_entry="S", limit=shortTP2, stop=shortSL, qty_percent=tp2Pct)
// ---------- Plots ----------
plot(useVwap ? vwap : na, "VWAP", linewidth=2)
plot(emaFastHTF, "HTF EMA Fast", color=color.new(color.green, 0))
plot(emaSlowHTF, "HTF EMA Slow", color=color.new(color.red, 0))
// Visual markers for rejection candles
plotshape(bullReject, title="Bull Rejection", style=shape.triangleup, location=location.belowbar, size=size.tiny, color=color.new(color.green, 0), text="BR")
plotshape(bearReject, title="Bear Rejection", style=shape.triangledown, location=location.abovebar, size=size.tiny, color=color.new(color.red, 0), text="SR")
// ---- Entry-ready signals (visual) ----
plotshape(longCond, title="LONG READY", style=shape.labelup, location=location.belowbar, text="LONG", color=color.new(color.green, 0), textcolor=color.white, size=size.tiny)
plotshape(shortCond, title="SHORT READY", style=shape.labeldown, location=location.abovebar, text="SHORT", color=color.new(color.red, 0), textcolor=color.white, size=size.tiny)
plot(longCond ? longStopPrice : na, "Long Stop Entry", style=plot.style_linebr, linewidth=2)
plot(shortCond ? shortStopPrice : na, "Short Stop Entry", style=plot.style_linebr, linewidth=2)
// =====================================================
// ADX DISPLAY (for visibility only)
// =====================================================
showADX = input.bool(true, "Show ADX (pane)", group="Signals / Alerts")
adxPlot = showADX ? adx : na
plot(adxPlot, title="ADX (5m)", color=color.new(color.orange, 0), linewidth=2)
// Reference lines
hline(20, "ADX 20", color=color.new(color.green, 60))
hline(35, "ADX 35", color=color.new(color.yellow, 60))
hline(45, "ADX 45", color=color.new(color.red, 60))