解密开盘区间突破(ORB)日内交易策略·综合运用
(3/3)·从单一信号到投资组合,用α、β与VWAP把ORB策略的边界和局限一次说清
VWAP与买卖执行的代码骨架
下面这段逻辑把时间窗判定、买卖下单和VWAP计算拆成了独立函数,直接在MT5里建EA就能跑通主干。 时间判定只用了小时和分钟的硬比较:当当前小时大于等于结束小时且分钟也达标时返回true,否则false。这种写法不考虑跨日,仅适合日内固定收尾的场景。 卖出函数先取SYMBOL_BID,用NormalizeDouble按品种小数位规整,止损价按 bid*(1+slp) 算,slp是外部止损比例;仓位由 calclots(bid*slp) 得出,最后 trade.Sell 带止损进场。买入对称处理,用SYMBOL_ASK和 ask*(1-slp)。 VWAP函数从指定K线索引 startCandle 向下循环到1(排除当前K线),典型价取 (High+Low+Close)/3,乘成交量累加得sumPV,体积累加sumV,最终 vwap = sumPV/sumV;若sumV为0返回0.0。回测时可观察:在EURUSD的M15上,用最近50根K线算VWAP,价格偏离均值±0.3%往往对应短期均值回归概率偏高,但外汇高杠杆下仍可能继续滑出。
if (currentHour >= endHour && currentMinute >= endMinute) class="kw">return true; else class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Sell execution function | class=class="str">"cmt">//+------------------------------------------------------------------+ 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Buy execution function | class=class="str">"cmt">//+------------------------------------------------------------------+ 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get VWAP function | class=class="str">"cmt">//+------------------------------------------------------------------+ 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 class=class="str">"cmt">// Loop from the starting candle index down to class="num">1 (excluding current candle) for(class="type">int i = startCandle; i >= class="num">1; i--) { class=class="str">"cmt">// Calculate typical price: (High + Low + Close) / class="num">3 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=class="str">"cmt">// Get volume and update sums class="type">long volume = iVolume(_Symbol, PERIOD_CURRENT, i); sumPV += typicalPrice * volume; sumV += volume; } class=class="str">"cmt">// Calculate VWAP or class="kw">return class="num">0 if no volume if(sumV == class="num">0) class="kw">return class="num">0.0; class="type">class="kw">double vwap = sumPV / sumV; class=class="str">"cmt">// Plot the dot
◍ 锚定会话起点与绘制 VWAP 标记
在 MT5 自定义指标里,把 VWAP 数值落进图表不能只靠返回 double,还得在当根 K 线时间上挂一个可视对象。下面这段把当前 Bar 时间取出来拼成唯一对象名,再用 OBJ_ARROW 画绿点,颜色、线型、宽度分别写死为 clrGreen、STYLE_DOT、1。 datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); // 取当前图表品种与周期的第0根Bar时间 string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES); // 用分钟级时间字符串避免重名 ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap); // 在currentBarTime处、vwap价格上建箭头对象 ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen); // 绿色 ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); // 点状 ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); // 点大小为1 return vwap; // 把算好的VWAP交回调用处 会话区间的界定靠 getSessionStartIndex:从 i=1 开始向后扫最多 1000 根 Bar,一旦某根本地时间 hour 等于 startHour 且 min 等于 30 就记为会话起点索引并 break。若你的品种每天 9:30 开盘,把 startHour 设为 9,就能抓到约 9:30 那根。 getBarShiftForTime 则反过来用:给定某日 day_start,把时分秒改写到目标 hour/minute,StructToTime 合成时间后丢给 iBarShift(PERIOD_M1, true),返回距现在的 M1 Bar 偏移量。配合 getUpperBand 里 iTime(_Symbol, PERIOD_D1, 0) 拿到的今日零点时间,可进一步定位 9:30 开盘价做通道上轨。外汇与贵金属杠杆高,这类时段锚定只描述概率性结构,实盘前请在策略测试器用历史数据核对偏移是否因节假日跳空而失效。
class="type">class="kw">datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, class="num">0); class="type">class="kw">string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES); 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 == class="num">30) { sessionIndex = i; break; } } class="kw">return sessionIndex; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get the number of bars from now to market open | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int getBarShiftForTime(class="type">class="kw">datetime day_start, class="type">int hour, class="type">int minute) { class="type">MqlDateTime dt; TimeToStruct(day_start, dt); dt.hour = hour; dt.min = minute; dt.sec = class="num">0; class="type">class="kw">datetime target_time = StructToTime(dt); class="type">int shift = iBarShift(_Symbol, PERIOD_M1, target_time, true); class="kw">return shift; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get the upper Concretum band value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double getUpperBand() { class=class="str">"cmt">// Get the time of the current bar class="type">class="kw">datetime current_time = iTime(_Symbol, PERIOD_CURRENT, class="num">0); class="type">MqlDateTime current_dt; TimeToStruct(current_time, current_dt); class="type">int current_hour = current_dt.hour; class="type">int current_min = current_dt.min; class=class="str">"cmt">// Find today&class="macro">#x27;s opening price at class="num">9:class="num">30 AM class="type">class="kw">datetime today_start = iTime(_Symbol, PERIOD_D1, class="num">0);
「用14天波动锚定当日上轨」
要把「Concretum 上轨」画出来,核心是先拿到今天 9:30 那根 M1 柱的开盘价,再拿过去 14 天同一时段的相对波动做 sigma。若当天或历史数据里缺了 9:30 柱、或开盘价为 0,函数直接返回 0,避免在图表上喷无意义对象。 下面这段是上轨计算的主干:先定位今日 9:30 柱,再循环回看 i=1 到 14 的日线起点,分别取历史日 9:30 开盘与当前时分收盘,算 abs(close/open-1) 累加。valid_days 不足时同样返回 0,只有拿到有效样本才用均值当 sigma,上轨即 open_930_today*(1+sigma)。
class="type">int bar_at_930_today = getBarShiftForTime(today_start, class="num">9, class="num">30); if (bar_at_930_today < class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// 无9:30柱则退出 class=class="str">"cmt">// 取今日9:class="num">30 M1开盘价 class=class="str">"cmt">// 取今日9:class="num">30 M1开盘价 class="type">class="kw">double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// 价格无效则退出 class=class="str">"cmt">// 基于过去14天计算sigma class="type">class="kw">double sum_moves = class="num">0; class="type">int valid_days = class="num">0; for (class="type">int i = class="num">1; i <= class="num">14; i++) { class="type">class="kw">datetime day_start = iTime(_Symbol, PERIOD_D1, i); class="type">int bar_at_930 = getBarShiftForTime(day_start, class="num">9, class="num">30); class="type">int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < class="num">0 || bar_at_HHMM < class="num">0) class="kw">continue; class=class="str">"cmt">// 柱不存在则跳过 class="type">class="kw">double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); class="type">class="kw">double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == class="num">0) class="kw">continue; class=class="str">"cmt">// 开盘价无效则跳过 class="type">class="kw">double move = MathAbs(close_HHMM / open_930 - class="num">1); sum_moves += move; valid_days++; } if (valid_days == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// 无有效数据退出 class="type">class="kw">double sigma = sum_moves / valid_days; class=class="str">"cmt">// 计算上轨 class="type">class="kw">double upper_band = open_930_today * (class="num">1 + sigma); class=class="str">"cmt">// 在上轨位画蓝点 class="type">class="kw">string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(class="num">0, obj_name, OBJ_ARROW, class="num">0, current_time, upper_band); ObjectSetInteger(class="num">0, obj_name, OBJPROP_ARROWCODE, class="num">159); class=class="str">"cmt">// 圆点符号 ObjectSetInteger(class="num">0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(class="num">0, obj_name, OBJPROP_WIDTH, class="num">2); class="kw">return upper_band; }
class="type">int bar_at_930_today = getBarShiftForTime(today_start, class="num">9, class="num">30); if (bar_at_930_today < class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 if no class="num">9:class="num">30 bar exists class="type">class="kw">double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// No valid price class=class="str">"cmt">// Calculate sigma based on the past class="num">14 days class="type">class="kw">double sum_moves = class="num">0; class="type">int valid_days = class="num">0; for (class="type">int i = class="num">1; i <= class="num">14; i++) { class="type">class="kw">datetime day_start = iTime(_Symbol, PERIOD_D1, i); class="type">int bar_at_930 = getBarShiftForTime(day_start, class="num">9, class="num">30); class="type">int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < class="num">0 || bar_at_HHMM < class="num">0) class="kw">continue; class=class="str">"cmt">// Skip if bars don&class="macro">#x27;t exist class="type">class="kw">double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); class="type">class="kw">double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == class="num">0) class="kw">continue; class=class="str">"cmt">// Skip if no valid opening price class="type">class="kw">double move = MathAbs(close_HHMM / open_930 - class="num">1); sum_moves += move; valid_days++; } if (valid_days == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 if no valid data class="type">class="kw">double sigma = sum_moves / valid_days; class=class="str">"cmt">// Calculate the upper band class="type">class="kw">double upper_band = open_930_today * (class="num">1 + sigma); class=class="str">"cmt">// Plot a blue dot at the upper band level class="type">class="kw">string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(class="num">0, obj_name, OBJ_ARROW, class="num">0, current_time, upper_band); ObjectSetInteger(class="num">0, obj_name, OBJPROP_ARROWCODE, class="num">159); class=class="str">"cmt">// Dot symbol ObjectSetInteger(class="num">0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(class="num">0, obj_name, OBJPROP_WIDTH, class="num">2); class="kw">return upper_band; }
用14天波动算出当日下轨并标点
这段逻辑把「当天 9:30 开盘价」当作基准,再用过去 14 个交易日的同向时段波动幅度求平均,得到一条动态下轨。外汇与贵金属属高杠杆品种,这种统计带只反映历史波动概率,不预示方向。 先取当前时间拆出小时和分钟,定位今天 D1 起点后找 9:30 那根 M1 bar 的开盘价;若找不到或价格为 0 直接返回 0,避免脏数据污染后续计算。
| 循环 i=1 到 14 遍历历史日,每天同样取 9:30 开盘与当前时刻收盘,算 | 收盘/开盘 - 1 | 的绝对值累加。valid_days 计数跳过无 bar 或开盘为 0 的日期,最终 sigma = 累加值 / valid_days;若 14 天全无效也返回 0。 |
|---|
下轨 = 今开 × (1 - sigma),用 ObjectCreate 画一个 arrowcode 159 的红点,宽度 2,名字带时间戳防重名。你在 MT5 里把这段接进 OnCalculate,就能在子图看到随波动收缩或扩张的红色下轨点。
TimeToStruct(current_time, current_dt); class="type">int current_hour = current_dt.hour; class="type">int current_min = current_dt.min; class=class="str">"cmt">// Find today&class="macro">#x27;s opening price at class="num">9:class="num">30 AM class="type">class="kw">datetime today_start = iTime(_Symbol, PERIOD_D1, class="num">0); class="type">int bar_at_930_today = getBarShiftForTime(today_start, class="num">9, class="num">30); if (bar_at_930_today < class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 if no class="num">9:class="num">30 bar exists class="type">class="kw">double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// No valid price class=class="str">"cmt">// Calculate sigma based on the past class="num">14 days class="type">class="kw">double sum_moves = class="num">0; class="type">int valid_days = class="num">0; for (class="type">int i = class="num">1; i <= class="num">14; i++) { class="type">class="kw">datetime day_start = iTime(_Symbol, PERIOD_D1, i); class="type">int bar_at_930 = getBarShiftForTime(day_start, class="num">9, class="num">30); class="type">int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < class="num">0 || bar_at_HHMM < class="num">0) class="kw">continue; class=class="str">"cmt">// Skip if bars don&class="macro">#x27;t exist class="type">class="kw">double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); class="type">class="kw">double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == class="num">0) class="kw">continue; class=class="str">"cmt">// Skip if no valid opening price class="type">class="kw">double move = MathAbs(close_HHMM / open_930 - class="num">1); sum_moves += move; valid_days++; } if (valid_days == class="num">0) class="kw">return class="num">0; class=class="str">"cmt">// Return class="num">0 if no valid data class="type">class="kw">double sigma = sum_moves / valid_days; class=class="str">"cmt">// Calculate the lower band class="type">class="kw">double lower_band = open_930_today * (class="num">1 - sigma); class=class="str">"cmt">// Plot a red dot at the lower band level class="type">class="kw">string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(class="num">0, obj_name, OBJ_ARROW, class="num">0, current_time, lower_band); ObjectSetInteger(class="num">0, obj_name, OBJPROP_ARROWCODE, class="num">159); class=class="str">"cmt">// Dot symbol ObjectSetInteger(class="num">0, obj_name, OBJPROP_COLOR, clrRed); ObjectSetInteger(class="num">0, obj_name, OBJPROP_WIDTH, class="num">2); class="kw">return lower_band;
◍ 按止损点数反推手数的实盘函数
在 MT5 里做固定风险比例的仓位管理,核心是把「账户愿意亏多少」和「这笔单子止损占了几跳」绑在一起算。下面这个函数把风险百分比、止损点数、合约跳值一次性揉成可下单数,省得每次手动估手数。 函数先取账户净值乘以 risk 百分比得到单次风险金额;再拿当前品种的 tick_size 与 tick_value,把止损点数的 slpoints 换算成「每最小步长要亏多少美元」。用风险额除以单步亏损,向下取整到 lotstep 倍数,就是理论手数。 最后两行把算出的 lots 夹在品种允许的 SYMBOL_VOLUME_MIN 与 SYMBOL_VOLUME_MAX 之间,避免报「无效交易量」错误。外汇与贵金属杠杆高,错手数可能瞬间吃掉本金,开仓前务必在策略测试器用真实点差跑一遍。 回测样本里该仓位模块配合趋势过滤,风险参数 risk=1 时样本斜率 α=1.6562、截距 β=-0.1183,说明手数随止损拉长而线性收缩,但实际滑点未计入,实盘概率上会偏乐观。
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; }
「组合分散化不是堆数量而是看相关性」
三种源自股市开盘区间突破的变体策略,回测都验证了能盈利,但单拎出来最大回撤都超过15%。把它们按一致风险分配凑成一个组合后,回测最大回撤压到约10%,净值和回撤曲线能在不同时间段互相抵消——这是算法交易里分散化降低整体风险的直接证据。 必须明确,分散化不是越多越好,无相关性才是核心。像所有加密资产上跑同一套策略就没意义,广义上它们高度同向。回测里的相关性也看周期,日收益和月收益算出来能差很多;遇市场结构突变,策略间相关性会扭曲,所以有人更信实盘相关性相对回测的偏移来判断优势是否衰减。 拿这三种策略的月度收益算相关性矩阵,平均约0.3,低于0.5算可接受但不够理想。它们都带多空、却还是正相关,根子在同一标的资产上做相似逻辑。3000多笔样本下,回撤收益按月分布均匀,说明没极端环境捣乱,但也提示:这组合同类度高,真要扩敞口得搭外部不同策略,而不是内部再加同类。 外汇和贵金属这类高杠杆品种若套用思路,先想清楚跨品种相关性是不是真低,否则分散只是账面安慰,实盘一遇联储决议或流动性断档,相关性瞬间拉满,回撤可能远超回测的10%。
画得少,看得清
前面三套开盘区间突破 EA 把信号规则、MQL5 源码和回测统计都摊开了,核心结论其实就一句:单策略在 NQ 一类期货上默认参数能跑出文献结果,但外汇盘用模拟账户数据复现,ORB1 偶尔对齐、ORB2 与 ORB3 多半对不上——评论区已有四位读者在 2025年10月到2026年2月间报告同类偏差。 分散化才是原文真正落点:把三套逻辑不相关的 ORB 摞进一个组合,回测波动比单跑任一套更平,但外汇与贵金属本身杠杆高、滑点跳空频繁,组合也只是降低而非消除爆仓概率。 真要动手,先把 ORB1.mq5 拖进 MT5 用期货数据验一遍,再换你的经纪商外汇 tick 看偏移多少;有空就把 Wu簡單 改的那版「指定时间开仓后持 N 根 bar 平仓」逻辑移植到 ORB2 试水,比盲信回测报告实在。