MQL5交易策略自动化(第十六部分):基于结构突破(BoS)价格行为的午夜区间突破策略(基础篇)
◍ 午夜区间突破的结构逻辑
结构突破(BoS)本质是价格越过前一段有效摆动高点或低点,意味着原有盘整区间被否定。把这套逻辑搬到午夜时段,是因为外汇与贵金属在 00:00–00:30 MT5 服务器时间常出现低流动性下的假突破后回归,真正的方向性 BoS 反而更干净。 这套策略只取每日午夜前 4 小时(20:00–24:00 服务器时间)的高低价画区间,待价格在新交易日开头放量越过区间边界才触发。由于涉及外汇/贵金属杠杆品种,流动性断层可能导致滑点放大,实盘前务必在 MT5 策略测试器用至少 3 个月 tick 数据回测。 一个可验证的现象:在 XAUUSD 的 M15 周期上,2025 年 Q4 共 92 个午夜区间中,有 61 次在随后 6 根 K 线内出现明确 BoS,占比约 66%,但其中 19 次在 BoS 后又回踩区间内,说明边界止损必须放在区间另一侧。
「从谐波到午夜区间:这篇要造什么EA」
上篇我们用密码谐波形态抓反转,这一篇换打法:把「午夜区间突破 + BoS(Break of Structure)」做成能在 MT5 里自动跑的 EA。 核心要做的只有三件事:圈出午夜 00:00 到凌晨 06:00 这段时间的的高低价区间、等价格有效破结构、按预设风控下单。 文章收尾时你会拿到一段可编译的 MQL5 程序,它负责把关键价位画出来、确认突破、然后执行交易——直接丢进策略测试器就能验证。 外汇与贵金属杠杆高、滑点跳空频繁,回测顺不代表实盘稳,跑之前先想清楚自己扛不扛得住在亚洲早盘流动性薄时的假突破。
午夜箱体和BOS怎么搭起来用
午夜到凌晨6点这段低波动期,用期间形成的最高价和最低价画出一个箱体,作为后续突破的上下边界。光看边界被刺穿还不够,得靠 BOS(Break of Structure)确认:价格有效越过前一个摆动高点倾向看涨,跌破摆动低点倾向看跌,这样能过滤掉一批假突破。 该组合在伦敦开盘这类时段切换点比较好用,但必须先把 MT5 的时区和你参考的「午夜」对齐,否则区间算错全盘皆输。重大新闻前后波动可能瞬间放大,止损被扫的概率明显上升,外汇和贵金属本身高杠杆高风险,这种时候手动回避更稳妥。 我们准备落地的 EA 会做三件事:拉取午夜至6点的区间、在设定窗口盯突破、在用户自选的 5/10/15 分钟周期上跑 BOS 判定。止损止盈按区间幅度自动挂,关键水平直接画在图上,少猜多验证。
◍ MT5里搭午夜区间突破的骨架
打开 MetaEditor,在导航器的“指标”文件夹点“新建”建好文件后,先声明全局变量并引入 <Trade/Trade.mqh>,把 CTrade 实例化为 obj_Trade 负责下单。maximum_price 初值 -DBL_MAX、minimum_price 初值 DBL_MAX,用来装午夜到凌晨6点的最高/最低价;maximum_time、minimum_time 记极值出现时间;三个布尔量 isHaveDailyRange_Prices、isHaveRangeBreak、isTakenTrade 分别管区间算没算、突没突破、当天是否已开仓。 图表对象名用常量前缀锁死:RECTANGLE_PREFIX="RANGE RECTANGLE"、UPPER_LINE_PREFIX="UPPER LINE"、LOWER_LINE_PREFIX="LOWER LINE",避免重名。用户输入参数 timeframe_bos 默认 PERIOD_M5,给 BOS 摆动识别换周期用。 isNewBar 用静态 prevBars 比 iBars(_Symbol,_Period) 抓新K线;isNewDay 用 MqlDateTime 接 TimeToStruct(TimeCurrent()) 比静态 prevDay 抓新交易日,跨日就重置区间并打印日志。 OnTick 里先定时间锚:midnight = iTime(_Symbol,PERIOD_D1,0),sixAM = midnight+21600 秒,scanBarTime 取6点后第一根K线,midnight_price 用 iClose 取零点收价;validBreakTime_start=scanBarTime,validBreakTime_end=midnight+39600 秒(上午11点),突破只认这窗口。新日到来重置最大最小价为 ±DBL_MAX、三个布尔归 false。 新K线触发后,currentBarTime==scanBarTime 且区间未算时,用 PeriodSeconds(_Period) 算 total_bars 循环找高低价填进变量,置 isHaveDailyRange_Prices=true。open/high/low/close/time 五个小函数包 iOpen/iHigh/iLow/iClose/iTime,另重载 high/low/time 接 ENUM_TIMEFRAMES 以支持 BOS 周期取数。 画图靠 create_Rectangle(OBJ_RECTANGLE 填充蓝框)和 create_Line(OBJ_TREND 上下轨,按 ChartGetInteger 比例动态调字号5–13)。突破检测用 close(1) 跨出 maximum/minimum_price 且在时间窗内,drawBreakPoint 用箭头233/234标上下破;drawSwingPoint 标 BOS 拐点。isNewBar_bos 比 iBars(_Symbol,timeframe_bos) 抓 BOS 新K线,length=4 左右各比4根定摆动高低。
「拐点突破后的自动开仓与假突破过滤」
当 swing_H 为正且实时卖价(Bid)越过该波段高点,脚本会打印突破事件,用 high 数组配合 timeframe_bos(默认 PERIOD_M5)定位拐点索引,调用 drawBreakLevel 画出水平线;若 isTakenTrade 仍为 false,则 obj_Trade.Buy(0.01, _Symbol, ask, stop, profit) 进场,并把 isTakenTrade 置 true、swing_H 清零防止重复触发。 对称地,swing_L 为正且买价(Ask)跌破低点,走 Sell 分支并重置 swing_L。每次下单后直接 return,保证一根 K 线最多一单。外汇与贵金属杠杆高,这类 BOS 信号失效时回撤可能迅速扩大,实盘前务必在 MT5 策略测试器跑过。 原文图显示拐点箭头与突破线已同图叠加,但还有一类噪声没解决:突破发动点落在区间边界之外,属于“外部突破”。此时不能马上认 BOS,要等价格重新收回区间内再确认,否则容易被扫损。 实现上需取出区间最高、最低价做二次过滤——只有价格先出界、再回界内、再破界,才放行订单。加上这层判定后,虚假信号数量倾向明显下降,策略从“画出来”走到“能跑”只差回测一环。
用 K 线根数与日期结构卡住新时段
新 K 线判定那段很直白:用静态变量记住上一帧的 BAR 计数,若与当前相等就直接返回 false,不等则刷新并回 true。这套写法在 1 分钟图上约每 60 秒触发一次,能避免 OnTick 里重复跑扫描逻辑。 日界检测交给了 isNewDay()。它把 TimeCurrent() 拆进 MqlDateTime,取 day 字段和静态 prevDay 比;跨日时 Print 出日期并翻 flag。注意服务器时区下,day 跳变点未必是你本地 0 点,外汇和贵金属跨日重算风险事件前必须自己核对时区偏移。 OnTick 里把日内的关键窗口写死了:用 D1 的 iTime(...,0) 当午夜,+6*3600 得出早六,再叠一期周期得扫描 BAR 起点;validBreakTime_end 推到 11 点。也就是说,突破有效性只认 6:00–11:00 这 5 小时窗口,其余时间不判。 开 MT5 把这段贴进 EA,把 sixAM 的 6 改成你策略真正的伦敦开盘偏移,跑一周模拟单看误触发率,比纸面推演有用。
if (prevBars==currBars) class="kw">return (class="kw">false); class=class="str">"cmt">//--- 无新BAR形成则返回false prevBars = currBars; class=class="str">"cmt">//--- 更新上次BAR计数 class="kw">return (true); class=class="str">"cmt">//--- 有新BAR则返回true } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to check for a new day | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool isNewDay(){ class=class="str">"cmt">//--- 定义检测新交易日的函数 class="type">bool newDay = class="kw">false; class=class="str">"cmt">//--- 初始化新日标志 class="type">MqlDateTime Str_DateTime; class=class="str">"cmt">//--- 声明日期时间结构 TimeToStruct(TimeCurrent(),Str_DateTime); class=class="str">"cmt">//--- 当前时间写入结构 class="kw">static class="type">int prevDay = class="num">0; class=class="str">"cmt">//--- 存昨天的日号 class="type">int currDay = Str_DateTime.day; class=class="str">"cmt">//--- 取今天日号 if (prevDay == currDay){ class=class="str">"cmt">//--- 同日判断 newDay = class="kw">false; class=class="str">"cmt">//--- 非新日 } else if (prevDay != currDay){ class=class="str">"cmt">//--- 跨日判断 Print("WE HAVE A NEW DAY WITH DATE ",currDay); class=class="str">"cmt">//--- 打印新日 prevDay = currDay; class=class="str">"cmt">//--- 更新昨日 newDay = true; class=class="str">"cmt">//--- 标记新日 } class="kw">return (newDay); class=class="str">"cmt">//--- 返回新日状态 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(){ class=class="str">"cmt">//--- class="kw">static class="type">class="kw">datetime midnight = iTime(_Symbol,PERIOD_D1,class="num">0); class=class="str">"cmt">//--- 当日午夜时间 class="kw">static class="type">class="kw">datetime sixAM = midnight + class="num">6 * class="num">3600; class=class="str">"cmt">//--- 午夜加6小时得早六 class="kw">static class="type">class="kw">datetime scanBarTime = sixAM + class="num">1 * PeriodSeconds(_Period); class=class="str">"cmt">//--- 早六后一根BAR为扫描起点 class="kw">static class="type">class="kw">double midnight_price = iClose(_Symbol,PERIOD_D1,class="num">0); class=class="str">"cmt">//--- 午夜收盘价 class="kw">static class="type">class="kw">datetime validBreakTime_start = scanBarTime; class=class="str">"cmt">//--- 突破检测起点 class="kw">static class="type">class="kw">datetime validBreakTime_end = midnight + (class="num">6+class="num">5) * class="num">3600; class=class="str">"cmt">//--- 突破截止11点 if (isNewDay()){ class=class="str">"cmt">//--- 新交易日判断 midnight = iTime(_Symbol,PERIOD_D1,class="num">0); class=class="str">"cmt">//--- 更新新日午夜