解密开盘区间突破(ORB)日内交易策略·进阶篇
(2/3)· 跳过基础概念,直接拆解 VWAP 跟踪、通道突破与组合分散化的统计表现
用VWAP加300均線過濾噪音的M15趨勢跟蹤
這套做法本質是把成交量加權均線當成日內多空分界:15分鐘K線收盤價同時站上VWAP與300周期SMA才做多,同時跌破才做空。原論文在一分鐘跑、不設止損,但CFD手續費吃掉了頻繁進出的空間,實測M15配合0.8%止損、每筆風險2%更穩。 回測五年USTEC差價合約,收益率約501%,最大回撤16%,夏普3.57、盈虧比1.26。勝率只有42%屬趨勢跟蹤常態,利潤靠少數大波段拉回,外匯與貴金屬槓桿更高,實盤可能觸發更頻繁的止損。 平均每日超過一筆交易,因為盤中價格再穿越VWAP就允許重新進場;α值約為開盤突破策略的三倍,可能來自更短持倉與同日多空分散。下方EA代碼直接掛MT5的USTEC-M15就能驗。
class=class="str">"cmt">//USTEC-M15 class="macro">#include <Trade/Trade.mqh> CTrade trade; input class="type">int startHour = class="num">18; input class="type">int startMinute = class="num">35; input class="type">int endHour = class="num">23; input class="type">int endMinute = class="num">45; input class="type">class="kw">double risk = class="num">2.0; input class="type">class="kw">double slp = class="num">0.008; input class="type">int MaPeriods = class="num">300; input class="type">int Magic = class="num">0; class="type">int barsTotal = class="num">0; class="type">int handleMa; class="type">class="kw">double lastClose=class="num">0; class="type">class="kw">double lot = class="num">0.1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,class="num">0,MODE_SMA,PRICE_CLOSE); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|On tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; class="type">bool NotInPosition = true; class="type">class="kw">double ma[]; CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma); if(MarketOpened()&&!MarketClosed()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,class="num">1); class="type">int startIndex = getSessionStartIndex(); class="type">class="kw">double vwap = getVWAP(startIndex);
「用 VWAP 与均线交叉管住持仓和开仓」
这段逻辑把持仓巡检和时段开关揉在一起:先倒序扫一遍所有持仓,只处理 Magic 与当前品种匹配的那些;若多单且收盘价跌破 VWAP、或空单且收盘价升破 VWAP,就直接平掉,否则把 NotInPosition 标记为 false,说明场上还有同策略仓位。 平仓之后才看新信号:无持仓时,收盘价低于 VWAP 且同时低于 ma[0] 才执行 sell;高于 VWAP 且高于 ma[0] 才执行 buy。把 VWAP 当动态中轴、ma[0] 当趋势确认,两个条件都满足才动手,能过滤掉一半以上的毛刺穿越。 市场关闭判断独立成函数:MarketClosed() 取服务器时间,小时与分钟分别大于等于 endHour / endMinute 才返回 true,此时无条件把同策略持仓全平。外汇与贵金属杠杆高,这种收盘强平能避免周末跳空把账户直接掀翻,但跳空仍可能触发滑点亏损。 MarketOpened() 同理比对 startHour / startMinute,只在开启时段内允许新信号。开 MT5 把 startHour、endHour 换成你盯的盘面活跃段(比如伦敦盘 08:00 起),回测一下 VWAP 参数对平仓频率的影响。
for(class="type">int i = PositionsTotal()-class="num">1; i>=class="num">0; i--){ class="type">ulong pos = PositionGetTicket(i); class="type">class="kw">string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){ if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); else NotInPosition=false; } } if(lastClose<vwap&&NotInPosition&&lastClose<ma[class="num">0])executeSell(); if(lastClose>vwap&&NotInPosition&&lastClose>ma[class="num">0]) executeBuy(); } if(MarketClosed()){ for(class="type">int i = PositionsTotal()-class="num">1; i>=class="num">0; i--){ class="type">ulong pos = PositionGetTicket(i); class="type">class="kw">string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } class="type">bool MarketOpened() { class="type">class="kw">datetime currentTime = TimeTradeServer(); class="type">MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); class="type">int currentHour = timeStruct.hour; class="type">int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)class="kw">return true; else class="kw">return false; } class="type">bool MarketClosed() { class="type">class="kw">datetime currentTime = TimeTradeServer(); class="type">MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); class="type">int currentHour = timeStruct.hour; class="type">int currentMinute = timeStruct.min; if (currentHour >= endHour && currentMinute >= endMinute)class="kw">return true; else class="kw">return false; }
◍ 挂单执行与VWAP的底层写法
EA里卖单和买单的触发函数,核心是先抓实时报价再做精度归一。executeSell() 取 SYMBOL_BID,乘以 (1+slp) 算止损位,slp 是外部传入的止损比例参数;executeBuy() 对称地取 SYMBOL_ASK 乘 (1-slp)。两个函数都调 NormalizeDouble 按 _Digits 截断,避免报价小数位不匹配导致 MT5 拒单。 仓位不是写死的,lot = calclots(bid*slp) 把止损幅度当风险基数传给计算函数,意味着止损空间越大、单手开得越小。外汇与贵金属杠杆高,这种按波动距离反推手数的逻辑能压住尾部风险,但参数 slp 设太大仍可能触发平台最小手数限制。 VWAP 没有用内置指标句柄,而是从 startCandle 往回循环到 1(故意排除当前未收线蜡烛)。每根用 (High+Low+Close)/3 当典型价,乘 iVolume 累加 sumPV,除以 sumV 得到成交量加权均价。若 sumV 为 0 直接返 0.0,实盘里仅出现在无成交的冷清品种或周末衔接 Bar。 函数末尾用 iTime(...,0) 拿当前 Bar 时间,拼成 'VWAP'+TimeToString(...,TIME_MINUTES) 的对象名,准备在图表画点。你可以直接把这段抄进 MT5 的 include 区,改 slp 看止损价跳动是否符合预期。
class="type">void executeSell() { class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); class="type">class="kw">double sl = bid*(class="num">1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } class="type">void executeBuy() { class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); class="type">class="kw">double sl = ask*(class="num">1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } class="type">class="kw">double getVWAP(class="type">int startCandle) { class="type">class="kw">double sumPV = class="num">0.0; class=class="str">"cmt">// Sum of(price * volume) class="type">long sumV = class="num">0.0; class=class="str">"cmt">// Sum of volume for(class="type">int i = startCandle; i >= class="num">1; i--) { class="type">class="kw">double high = iHigh(_Symbol, PERIOD_CURRENT, i); class="type">class="kw">double low = iLow(_Symbol, PERIOD_CURRENT, i); class="type">class="kw">double close = iClose(_Symbol, PERIOD_CURRENT, i); class="type">class="kw">double typicalPrice = (high + low + close) / class="num">3.0; class="type">long volume = iVolume(_Symbol, PERIOD_CURRENT, i); sumPV += typicalPrice * volume; sumV += volume; } if(sumV == class="num">0) class="kw">return class="num">0.0; class="type">class="kw">double vwap = sumPV / sumV; class="type">class="kw">datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, class="num">0); class="type">class="kw">string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES);
从VWAP箭头到按风险反推仓位
这段脚本把前一步算出的 VWAP 值直接画成绿点:用 OBJ_ARROW 在当前 Bar 时间位上钉一个点,颜色 clrGreen、线型 STYLE_DOT、宽度 1,纯视觉标记,方便肉眼核对 VWAP 与价格的距离。外汇与贵金属杠杆高,这类标记只作参考,不代表方向判定。 找会话起点的逻辑靠 getSessionStartIndex:从 i=1 往上翻最多 1000 根 Bar,用 TimeToStruct 拆时间,碰到 hour 等于 startHour 且 min 等于 startMinute-5 就停下,返回该 Bar 索引。注意它写死往前偏移 5 分钟,若你的平台会话开始定义不同,这里要改。 calclots 才是实盘有用的部分:先取账户余额乘 risk% 得风险金额,再用 slpoints 换算每最小仓位步距的亏损额,MathFloor 取整后夹在品种最小/最大成交量之间。回测中 α=4.8714、β=0.0985 是拟合参数,换品种大概率要重估。 开 MT5 把这段粘进 EA,把 risk 和外部 startHour/startMinute 接好,跑 EURUSD 的 M15 看绿点位置和仓位输出是否符合预期。
ObjectCreate(class="num">0, objName, OBJ_ARROW, class="num">0, currentBarTime, vwap); ObjectSetInteger(class="num">0, objName, OBJPROP_COLOR, clrGreen); class=class="str">"cmt">// Green dot ObjectSetInteger(class="num">0, objName, OBJPROP_STYLE, STYLE_DOT); class=class="str">"cmt">// Dot style ObjectSetInteger(class="num">0, objName, OBJPROP_WIDTH, class="num">1); class=class="str">"cmt">// Size of the dot class="kw">return vwap; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Find the index of the candle corresponding to the session open | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int getSessionStartIndex() { class="type">int sessionIndex = class="num">1; class=class="str">"cmt">// Loop over bars until we find the session open for(class="type">int i = class="num">1; i <=class="num">1000; i++) { class="type">class="kw">datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i); class="type">MqlDateTime dt; TimeToStruct(barTime, dt); if(dt.hour == startHour && dt.min == startMinute-class="num">5) { sessionIndex = i; break; } } class="kw">return sessionIndex; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate lot size based on risk and stop loss range | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double calclots(class="type">class="kw">double slpoints) { class="type">class="kw">double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / class="num">100; class="type">class="kw">double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); class="type">class="kw">double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); class="type">class="kw">double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); class="type">class="kw">double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; class="type">class="kw">double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); class="kw">return lots; } α:class="num">4.8714 β:class="num">0.0985
「开盘噪音区突破:用波动率通道抓供需失衡」
康克瑞图姆通道突破本质是在开盘时段过滤随机波动、只做真正趋势起点的噪音区间突破。通道基于前一日收盘或当日开盘价,再乘波动率系数画出上下轨,价格呆在轨内视为噪音,1分钟K线突破上轨做多、跌破下轨做空,收盘前全平。 信号规则很直接:开盘后向上破上轨买入,向下破下轨卖出;止损压在入场价1%处,同时挂VWAP作追踪止损,单笔风险设4%。这套逻辑最初用于SPY,这里沿用USTEC标的,结果会和原论文有偏差。 回测里交易频率和首个ORB策略接近,平均两个交易日才约1笔——很多时候价格就在噪音区里晃,不触发突破。因为用了VWAP动态追踪,胜率不到50%,但盈亏比1.3、夏普5.9,收益相对回撤算强劲。 它比买入持有表现略好,最大回撤只有后者一半,但回撤期更频繁,约50%的月份在回撤,盈利月收益却更高。与标的的相关性为-11%,轻微反向,适合想逆趋势找优势的人。外汇和贵金属盘外推时要明白:杠杆品种高风险,样本再大也得扛住长回撤才等得到肥尾。 代码里把交易窗口钉在18:35到23:55,maPeriod=400的SMA拿来近似VWAP参照;想验就开MT5挂USTEC的M1,把startHour改成你broker的开盘时间跑一遍。
class=class="str">"cmt">//USTEC-M1 class="macro">#include <Trade/Trade.mqh> CTrade trade; input class="type">int startHour = class="num">18; input class="type">int startMinute = class="num">35; input class="type">int endHour = class="num">23; input class="type">int endMinute = class="num">55; input class="type">class="kw">double risk = class="num">4.0; input class="type">class="kw">double slp = class="num">0.01; input class="type">int Magic = class="num">0; input class="type">int maPeriod = class="num">400; class="type">int barsTotal = class="num">0; class="type">int handleMa; class="type">class="kw">double lastClose=class="num">0; class="type">class="kw">double lastOpen = class="num">0; class="type">class="kw">double lot = class="num">0.1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,class="num">0,MODE_SMA,PRICE_CLOSE); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|On tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; class="type">bool NotInPosition = true; class="type">class="kw">double ma[]; CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma); if(MarketOpened()&&!MarketClosed()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,class="num">1); lastOpen = iOpen(_Symbol,PERIOD_CURRENT,class="num">1); class="type">int startIndex = getSessionStartIndex();
◍ 用 VWAP 与通道边界管住持仓和开仓
这段逻辑把出场和进场都绑在 VWAP 与上下轨上:先算好当前 VWAP,再倒序扫一遍持仓,只要是本 EA 的魔术码且符号对得上,价格收在 VWAP 另一侧就平掉,否则标记还在场中。 double vwap = getVWAP(startIndex); for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
| if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap) | (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); |
|---|
else NotInPosition=false; } } double lower = getLowerBand(); double upper = getUpperBand(); if(NotInPosition&&lastOpen>lower&&lastClose<lower&&lastClose<ma[0])executeSell(); if(NotInPosition&&lastOpen<upper&&lastClose>upper&&lastClose>ma[0]) executeBuy(); 上面对应的是: 多单收破 VWAP 或空单收上 VWAP 就平仓; 空仓时若蜡烛从下轨上方开盘、收在下轨下方且收在均线之下,倾向触发 sell,对称情况触发 buy。外汇与贵金属杠杆高,这种收线判定在跳空时可能连续误触,建议拿 MT5 历史数据跑一遍看平仓频率。 市场关闭分支更直接,只要 MarketClosed() 为真,同魔术码同符号的仓位全平,不跟行情赌周末。两个时间函数都读 TimeTradeServer() 再拆 hour/minute,startHour 与 startMinute 是外部变量,改这两个值就能平移交易窗口。 bool MarketOpened() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)return true; else return false; } bool MarketClosed() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min;
class="type">class="kw">double vwap = getVWAP(startIndex); for(class="type">int i = PositionsTotal()-class="num">1; i>=class="num">0; i--){ class="type">ulong pos = PositionGetTicket(i); class="type">class="kw">string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){ if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); else NotInPosition=false; } } class="type">class="kw">double lower = getLowerBand(); class="type">class="kw">double upper = getUpperBand(); if(NotInPosition&&lastOpen>lower&&lastClose<lower&&lastClose<ma[class="num">0])executeSell(); if(NotInPosition&&lastOpen<upper&&lastClose>upper&&lastClose>ma[class="num">0]) executeBuy(); } if(MarketClosed()){ for(class="type">int i = PositionsTotal()-class="num">1; i>=class="num">0; i--){ class="type">ulong pos = PositionGetTicket(i); class="type">class="kw">string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Detect if market is opened | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool MarketOpened() { class="type">class="kw">datetime currentTime = TimeTradeServer(); class="type">MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); class="type">int currentHour = timeStruct.hour; class="type">int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)class="kw">return true; else class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Detect if market is closed | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool MarketClosed() { class="type">class="kw">datetime currentTime = TimeTradeServer(); class="type">MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); class="type">int currentHour = timeStruct.hour; class="type">int currentMinute = timeStruct.min;