解密开盘区间突破(ORB)日内交易策略(基础篇)
◍ 开盘区间突破的底层逻辑
开盘区间突破(ORB)把一天最初 N 分钟的价格上下沿当作战场边界:向上破上沿做多,向下破下沿做空。这套思路假设早盘流动性集中释放,方向一旦选出,惯性可能延续到午盘。 在 MT5 上验证该逻辑时,常见做法是取北京时间 08:00–08:30(对应部分平台悉尼/东京重叠段)的 High/Low 作为基准区间。统计样本显示,EURUSD 在 2024 年有约 38% 的交易日在该区间形成后 2 小时内出现大于 20 点突破,其中假突破占比接近一半,说明单纯追破点位需叠加过滤。 外汇与贵金属属高杠杆高风险品种,ORB 信号失效时回撤可能快速吞噬日内利润,实盘前务必用策略测试器跑足一年 tick 数据。
ORB 策略到底在抓什么
开盘区间突破(ORB)的核心假设很直接:开盘后一小段时间形成的区间,是买卖双方对‘当前价格值不值’达成的临时共识区。价格一旦干净地突破这个区间上沿或下沿,方向偏好往往会被激活,后续延续的概率会抬升——但这是概率,不是必然,外汇和贵金属市场的高波动随时能吞掉这种偏好。 本系列改编自康克瑞图姆集团的三篇论文,不堆代码细节,重点放在‘怎么把论文里的策略复现出来、测出来’。每篇给出信号规则 + 统计表现,最后从组合层面看分散化价值。所有 EA 的 MQL5 源码都会放出,你可以直接拖进 MT5 改参数验证。 需要提醒的是,这类基于历史统计的优势在样本外可能衰减,实盘前务必用 MT5 策略测试器跑一遍你自己的品种和周期。
「时区、标的与αβVWAP的底子」
做日内策略回测,第一道坎是时区对齐。康克瑞图姆那套研究锁定美东 9:30–16:00 交易窗口,落到 MT5 服务器时间(UTC+2/3)就是 18:30–24:00。你跑之前必须按自己经纪商时区重算,否则样本整体偏移,结论直接废。 原始论文用 QQQ 作标的,但 QQQ 本身只是纳斯达克-100 的 ETF,指数不可直接交易。我们实测换成了 USTEC 差价合约——不持标的、带杠杆,贵金属和外币 CF D 同理,杠杆双向放大,属于高风险敞口,别当现货做。 α 和 β 是两个绕不开的基准量。α 是相对市场基准的超额收益,正 α 才说明策略有独特优势;β 衡量波动敏感度,=1 跟市场同喘,>1 更疯,<1 更钝。趋势性资产(指数、加密)常带方向偏差,不看 β 容易把贝塔当阿尔法。 αβ 用日收益率算:Ri 投资收益、Rf 无风险利率(常可忽略)、Rm 市场收益,协方差/方差取日频。公式摆下面,自己接历史数据就能跑。 VWAP 是成交量加权平均价,从开盘第一根 K 线起算,Pi 取收盘、Vi 取量。它比简单均线更公平,量大的价格权重高,算法交易里常当趋势过滤器、跟踪止损或穿越信号用。CFD 成交量因流动性商而异,但相对权重各平台应一致。 风险端用杠杆空间模型:每笔止损只亏账户固定比例,止损幅度按标的价格固定百分比走,单笔风险取整,目标最大回撤压在约 20% 内。测试窗拉满五年(2020-01-01 至 2025-01-01),够近够长,再拿买入持有做对照。
◍ 开盘五分钟K线配合350均线的突破玩法
开盘突破的思路抓的是盘初那波机构活动带来的方向惯性。原始论文用首根五分钟K线高低点做止损、挂10倍风险止盈,但对散户来说紧止损摩擦太大,且收盘前强平让10R几乎够不到,所以改成用350周期均线过滤市态、止损压到入场价1%、每笔风险2%。 改完的规则很直白:开盘五分钟后,若首根五分钟K线收阳且收盘价站上350均线就买;收阴且跌破350均线就卖;收盘前5分钟清空持仓。加上均线过滤后,每日交易数大概砍掉一半,多头触发更频繁且胜率偏高。 回测五年USTEC(M5)显示,策略盈亏比1.23、夏普2.81,最大回撤18%,最惨单月回撤10.2%;日收益与标的本身相关性仅0.9%,说明利润主要来自规则而非跟涨。外汇与贵金属杠杆高、滑点跳空频繁,直接搬参数前务必在MT5用历史数据自测一遍。 别把正态当圣经 代码里 startHour=18、startMinute=35 对应USTEC开盘,换品种要把这两个 input 改成你平台的盘初时间,否则 OnTick 里的 MarketOpen() 逻辑会对不上。
class=class="str">"cmt">//USTEC-M5 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">2.0; input class="type">class="kw">double slp = class="num">0.01; input class="type">int MaPeriods = class="num">350; 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 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,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">class="kw">double ma[]; CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma); if(MarketOpen()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,class="num">1); lastOpen = iOpen(_Symbol,PERIOD_CURRENT,class="num">1); if(lastClose<lastOpen&&lastClose<ma[class="num">0])executeSell();
用服务器时间卡死开平仓窗口
这段逻辑把交易动作严格绑在交易服务器时间的某分某秒上,而不是靠指标交叉持续触发。MarketOpen 与 MarketClose 都只比较 hour 和 minute 是否等于预设的 start/end 值,意味着每天只在那一分钟返回 true,其余时间机器人按兵不动。 平仓段遍历 PositionsTotal()-1 到 0,用 PositionGetTicket 取 ticket、PositionGetSymbol 核对品种,再判断 POSITION_MAGIC 是否匹配本 EA 的 Magic,避免误平手动单或其他 EA 的仓。外汇与贵金属杠杆高,这种按点平仓若遇滑点可能偏离预设分钟成交价,实盘前应在 MT5 策略测试器用真实点差回放验证。 executeSell 里止损算法是 bid*(1+slp),slp 为小数比例(如 0.01 即 1% 距),再用 NormalizeDouble 对齐品种小数位;lot 由 calclots(bid*slp) 推算,仓 size 与风险距离挂钩。把 startHour、startMinute、endHour、endMinute 和 slp 做成外部输入参数,你就能在不改代码的情况下切换时段与止损宽度。
if (lastClose>lastOpen&&lastClose>ma[class="num">0]) executeBuy(); } if(MarketClose()){ 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 MarketOpen() { 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 MarketClose() { 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; } 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() {
「按风险敞口反推开仓手数」
上面那段收盘逻辑里,真正决定仓位轻重的不是信号,而是 calclots 这个函数。它把账户余额、风险百分比和止损跨度绑在一起,用 tick 价值反推能扛多少手,外汇和贵金属的高杠杆特性下,这套算法直接决定一笔单子会不会过早被波动洗掉。 调用时先取实时卖价并归一化到品种精度,止损位按 ask*(1-slp) 算,slp 是止损比例;手数交给 calclots(ask*slp) 算出后直接 Buy。注意 slp 若设 0.0090,EURUSD 这类品种约对应 9 个点外止损,属偏紧设置,遇数据行情可能被秒触。 calclots 内部先拿 ACCOUNT_BALANCE*risk/100 当风险金,再用 SYMBOL_TRADE_TICK_SIZE 和 TICK_VALUE 把止损跨度折成每最小手步的金额。最后 MathFloor 向下取整乘 lotstep,并夹在品种最小最大手数之间——这避免了券商拒绝订单,也防止贪心超仓。 原文末附的 α=1.6017、β=0.0090 像是该策略拟合出的系数,前者可能关联均值回复强度,后者即上面说的止损比例。直接把这两个数塞进 EA 跑回测,比凭感觉调 slp 更靠谱,但实盘前务必在 MT5 策略测试器用历史数据验证一遍。
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">//| 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">1.6017 β:class="num">0.0090