使用MQL5开发基于震荡区间突破策略的EA(基础篇)
震荡区间突破EA的起步思路
在 MT5 里做震荡区间突破策略,核心是先框定一段价格收敛区,再等价格脱离该区后顺势进场。原文给出的基础样本在 2025 年 1 月 27 日发布,截至统计时已获 2273 次浏览、3 条讨论,说明这类朴素突破逻辑在实战社群里仍有高频关注度。 外汇与贵金属市场杠杆高、滑点突发行情多,区间被假突破扫损的概率不低,任何突破信号都只能当作「倾向触发」而非确定性入口。 开 MT5 后新建 EA,第一步通常是用 iHigh / iLow 抓取前 N 根 K 线的区间上下轨,再用当前收盘价与轨道做交叉判断,后面几节会逐步补完过滤与下单部分。
◍ 用 MQL5 把整单区间突破写成 EA
这篇要干的事很具体:拿 MQL5 在 MT5 上搓一个基于整单(横盘)区间突破的 EA。横盘期波动率压到低位,后面出方向时往往带一波像样的价格移动,这套逻辑抓的就是这种低波动后的释放。 原文给出的落地路径分五步:先讲策略概览,再写具体规则,接着是 MQL5 实现,然后跑回测,最后给结论。跟着走完,你能在自己的 MT5 里复刻一个可加载的突破型机器人,而不是只看概念。 环境上只认 MetaTrader 5 终端加 MQL5 编辑器这一组合,EA 文件最终在 MT5 里执行。外汇与贵金属品种波动剧烈、杠杆风险高,实盘前务必用策略测试器先验证。
「把震荡区间突破拆开看」
震荡区间(也叫交易区间)指价格在明确上下边界内横向摆动、无明显趋势的一段时期,波动率低,上沿是阻力、下沿是支撑。有经验的交易者常在这个阶段提前揣测突破点——区间结束后价格朝某一方向大幅移动的概率会抬升。 策略核心分三步:先用近期 N 根 K 线的最高/最低价圈出区间边界,时间框架按自己风格选,不强制;接着监控收盘价站上阻力或跌破支撑算突破(也有人用刺穿极值就算,偏剥头皮);最后按突破方向发单,本版不等待回调确认。 落地参数要给死:例如要求 700 点或 70 点价格范围内至少 10 根 K 线,区间才有效。开发时把「最高/最低扫描 + 绘图 + 有效性假设」写清楚,实盘每跳或每根新 K 线刷一次监控,触发即下单并带止损止盈。外汇与贵金属杠杆高、滑点凶,这类突破假突破频发,先用 MT5 历史中心回测再上实盘。
把震荡区间识别写进 MT5 的 EA 骨架
打开 MT5 终端后按 F4 进入 MetaEditor,Ctrl+N 调出 MQL 向导,选 Expert Advisor (template) 一路下一步,就能拿到一个空白 EA 文件。所有全局变量和交易对象都建议写在源码开头,方便后续 OnTick 里反复调用。 先在头部用 #include <Trade/Trade.mqh> 引入 CTrade 类,再声明 obj_Trade 对象,这是下单的唯一通道。区间名称用 #define rangeNAME "CONSOLIDATION RANGE" 固化成宏,避免每次画图手敲字符串出错;矩形坐标分两组:datetime 型的 TIME1_X1 / TIME2_X2 存时间轴,double 型的 PRICE1_Y1 / PRICE2_Y2 存价格轴。 全局里必须钉死两个整型参数:rangeBars=10 代表回看 10 根 K 线找高低点,rangeSizePoints=400 代表区间极限宽度 400 点(外汇点值 _Point 通常为 0.00001,即 40 美元/标准手量级的毛刺上限)。布尔旗标 isRangeExist 与 isInRange 初始 false,用来锁住「区间已画」和「价在区间内」两种状态。 OnTick 是每笔报价触发,但找区间只需每根 bar 跑一次。用 iBars 取 currBars,配合 static 整型 prevBars 和 static 布尔 isNewBar 做新柱判断:相等则跳过,不等才刷新 prevBars 并置 isNewBar=true,省掉同根 K 线内的重复扫描。 新 bar 且 isRangeExist==false 时,iTime(_Symbol,_Period,rangeBars) 取区间起点时间,iHighest 拿最高价柱索引再 iHigh 取价填 PRICE1_Y1;对称用 iLowest / iLow 填第二点。区间有效性 = (PRICE1_Y1-PRICE2_Y2)/_Point <= rangeSizePoints,过关才进 plotConsolidationRange 画图并把 isRangeExist 置 true、打印 "RANGE PLOTTED"。 绘图函数里 ObjectFind 返回负整数就 Create 一个 OBJ_RECTANGLE,否则用 0/1 修饰符更新两坐标后 ChartRedraw;填充与否看 OBJPROP_FILL 开关。实测编译后日志出现 RANGE PLOTTED 即代表 10 根内窄幅区间已被锚定,接下来才轮到突破监控。
class="macro">#include <Trade/Trade.mqh> CTrade obj_Trade; class="macro">#define rangeNAME "CONSOLIDATION RANGE" class="type">class="kw">datetime TIME1_X1, TIME2_X2; class="type">class="kw">double PRICE1_Y1, PRICE2_Y2; class="type">bool isRangeExist = class="kw">false, isInRange = class="kw">false; class="type">int rangeBars = class="num">10, rangeSizePoints = class="num">400; class="type">int OnInit(){ class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick(){ class="type">int currBars = iBars(_Symbol,_Period); class="kw">static class="type">int prevBars = iBars(_Symbol,_Period); class="kw">static class="type">bool isNewBar = class="kw">false; if(currBars == prevBars) isNewBar = class="kw">false; else { isNewBar = true; prevBars = currBars; } if(!isRangeExist && isNewBar){ TIME1_X1 = iTime(_Symbol,_Period,rangeBars); class="type">int highestHigh_BarIndex = iHighest(_Symbol,_Period,MODE_HIGH,rangeBars,class="num">1); PRICE1_Y1 = iHigh(_Symbol,_Period,highestHigh_BarIndex); TIME2_X2 = iTime(_Symbol,_Period,class="num">0); class="type">int lowestLow_BarIndex = iLowest(_Symbol,_Period,MODE_LOW,rangeBars,class="num">1); PRICE2_Y2 = iLow(_Symbol,_Period,lowestLow_BarIndex); if((PRICE1_Y1-PRICE2_Y2)/_Point <= rangeSizePoints){ isInRange = true; } if(isInRange){ plotConsolidationRange(rangeNAME,TIME1_X1,PRICE1_Y1,TIME2_X2,PRICE2_Y2); isRangeExist = true; Print("RANGE PLOTTED"); } } } class="type">void plotConsolidationRange(class="type">class="kw">string name,class="type">class="kw">datetime t1,class="type">class="kw">double p1,class="type">class="kw">datetime t2,class="type">class="kw">double p2){ if(ObjectFind(class="num">0,name)<class="num">0){ ObjectCreate(class="num">0,name,OBJ_RECTANGLE,class="num">0,t1,p1,t2,p2); ObjectSetInteger(class="num">0,name,OBJPROP_COLOR,clrSkyBlue); ObjectSetInteger(class="num">0,name,OBJPROP_FILL,true); ObjectSetInteger(class="num">0,name,OBJPROP_WIDTH,class="num">2); } else { ObjectSetDouble(class="num">0,name,OBJPROP_TIME,class="num">0,t1); ObjectSetDouble(class="num">0,name,OBJPROP_PRICE,class="num">0,p1); ObjectSetDouble(class="num">0,name,OBJPROP_TIME,class="num">1,t2); ObjectSetDouble(class="num">0,name,OBJPROP_PRICE,class="num">1,p2); ChartRedraw(class="num">0); } }
◍ 让矩形区间跟着K线活起来
静态区间最大的毛病是画完就死:价格一旦越过边界,对象不重绘,策略还按旧坐标判断突破,等于瞎眼下单。想让震荡区间有生命力,就得在每根已完成K线出现后重写它的边界与时间跨度。 当买价高过区间内记录的最高价,说明上边界要抬。代码里把 PRICE1_Y1 直接赋为当前买价,同时用 iTime(0) 抓当前时间刷新 TIME2_Y2,再调 plotConsolidationRange 重画。下边界跌破时镜像处理,把 PRICE2_Y2 设为新低。 如果价格既没破顶也没穿底,但新柱已经收线,也要把 TIME2_Y2 延到前一柱时间(iTime 索引 1),否则区间长度会越走越短。用 isNewBar 判新柱,命中就在终端打印一句“结束时间已扩展”,然后重绘。 实盘里这套逻辑跑在 OnTick:以 EURUSD 为例,最低低价 0.67911、区间上限 400 点(点值 0.00001)算得高位突破价 0.68311;买价报 0.68313 即触发多单,止损止盈各 500 点。外汇与贵金属波动剧烈,区间重绘不代表突破概率提升,仅降低漏判可能。 下面这段是区间更新相关的 MQL5 骨架,注意 OnTick 里 iBars 与 isNewBar 的写法,复制到 MT5 能直接编译看框架:
class="macro">#include <Trade/Trade.mqh> class=class="str">"cmt">// Include the trade library CTrade obj_Trade; class=class="str">"cmt">// Create an instance of the CTrade class class="macro">#define rangeNAME "CONSOLIDATION RANGE" class=class="str">"cmt">// Define the name of the consolidation range class="type">class="kw">datetime TIME1_X1, TIME2_Y2; class=class="str">"cmt">// Declare class="type">class="kw">datetime variables to hold range start and end times class="type">class="kw">double PRICE1_Y1, PRICE2_Y2; class=class="str">"cmt">// Declare class="type">class="kw">double variables to hold range high and low prices class="type">class="kw">datetime TIME1_X1; class="type">class="kw">datetime TIME2_Y2; class="type">class="kw">double PRICE1_Y1, PRICE2_Y2; class=class="str">"cmt">// Declare class="type">class="kw">double variables to hold range high and low prices class="type">bool isRangeExist = class="kw">false; class=class="str">"cmt">// Flag to check if the range exists class="type">bool isInRange = class="kw">false; class=class="str">"cmt">// Flag to check if we are currently within the range class="type">int rangeBars = class="num">10; class=class="str">"cmt">// Number of bars to consider for the range class="type">int rangeSizePoints = class="num">400; class=class="str">"cmt">// Maximum range size in points class="type">class="kw">double sl_points = class="num">500.0; class=class="str">"cmt">// Stop loss points class="type">class="kw">double tp_points = class="num">500.0; class=class="str">"cmt">// Take profit points class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(){ class=class="str">"cmt">//--- } class="type">int currBars = iBars(_Symbol,_Period); class=class="str">"cmt">// Get the current number of bars class="kw">static class="type">int prevBars = currBars; class=class="str">"cmt">// Static variable to store the previous number of bars class="kw">static class="type">bool isNewBar = class="kw">false;class=class="str">"cmt">// Static flag to check if a new bar has appeared if (prevBars == currBars){isNewBar = class="kw">false;} class=class="str">"cmt">// Check if the number of bars has not changed else {isNewBar = true; prevBars = currBars;} class=class="str">"cmt">// If the number of bars has changed, set isNewBar to true and update prevBars