价格行为分析工具开发(第二十八部分):开盘区间突破工具·进阶篇
(2/3)· 从闪崩教训到状态机实现,手把手拆开 ORB 策略的 EA 骨架与回测验证
◍ 开盘区间怎么被EA框出来
这套EA把开盘区间突破做成了全自动流程。每个时段一开始先记开盘价,随后在 RangeMinutes(默认 15 分钟)窗口里不断刷新最高、最低,窗口一关就用水平线加矩形把这段早期波动圈出来。 波动率不够它不下单。ATR 过滤器带可配周期和乘数,只有真实波幅过了阈值,突破信号才被放行,避免在窄幅垃圾行情里频繁假突破。 区间定型后走两步确认:价格先收在区间外,再回撤碰边界、随后再度收在外侧,这才算回测成立。此时 EA 在入场 K 线画箭头,按 ATR 算止损和目标,并推屏幕、邮件或推送警报。 仪表盘盯着重置节点——下一时段或午夜自动清状态、ATR 值和区间宽度,不用人手干预。外汇与贵金属杠杆高,信号失效时回撤可能快速扩大,验证前先用策略测试器跑历史时段。
「把ORB策略拆进MT5的类与状态机」
写MT5的EA,第一道关是文件头。#property strict不是摆设,它强制编译器走现代MQL5规则:禁止隐式类型转换、要求函数原型显式声明,能在编译期揪出不少隐蔽笔误。链接和描述字段则会直接显示在EA属性框里,省掉额外UI代码做说明。 输入参数这块,MQL5的input变量自动进属性对话框,ENUM_TIMEFRAMES这类枚举直接渲染成下拉菜单。想限制箭头字符,可以用自定义枚举ArrowSymbol{UP=233,DOWN=234}锁死选项,比让用户填任意Wingdings码更稳。 核心逻辑建议封装成类。CRangeCapture管开盘区间,提供Init()、Update()、IsDefined();注意MQL5类不自动调构造,必须手写Init()。hi/lo用double吃满64位浮点精度,外汇报价差个0.00001都算得清。CATRModule把iATR句柄缓存起来,Value()每tick只CopyBuffer一次,Release()里调IndicatorRelease()防指标泄漏——漏写这步,终端指标标签页会堆满僵尸句柄。 信号判定用CRetestSignal三个私有布尔值搞定突破-回踩-再突破,MqlRates用引用传参省拷贝。CDashboard用OBJ_LABEL做左上角面板,高频tick下可缓存上一次字符串,仅内容变了才ObjectSetString(),这点开销优化在实盘EA里很值钱。 OnInit()初始化失败就返INIT_FAILED,EA绝不以残态启动;OnDeinit()配对释放句柄和对象。OnTick()用static变量跨tick保持状态,switch(state)把策略切成捕捉、确认、回踩、结束四段——这是MQL5无协程下的标准写法。午夜重置用now-now%86400比hour==0靠谱,能躲时区与夏令时的差一天bug。 下面这段头文件与输入声明,直接拷进MetaEditor能验证枚举下拉与strict报错行为:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ORB Breakout EA| class=class="str">"cmt">//| Copyright class="num">2025, MetaQuotes Ltd.| class=class="str">"cmt">//| [MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.0" class="macro">#class="kw">property description "Opening‑Range Breakout with ATR confirmation" class="macro">#class="kw">property strict class=class="str">"cmt">//--- session & range input class="type">int SessionIndex = class="num">0; class=class="str">"cmt">// class="num">0 = first session input class="type">int RangeMinutes = class="num">15; class=class="str">"cmt">// minutes to capture range class=class="str">"cmt">//--- ATR filter input ENUM_TIMEFRAMES TF = PERIOD_M1; class=class="str">"cmt">// ATR timeframe
用类把开盘区间和ATR封装起来
ORB 策略的核心是先抓一段固定时间的极值,再用 ATR 给突破加一层波动过滤。下面这段把两块逻辑拆成两个类,主程序只要调接口,不用管内部细节。 CRangeCapture 负责在 RangeMinutes 分钟内持续刷新最高价和最低价。Init 用开盘时刻和首根价格定基准,Update 只在时间窗内更新 hi/lo,IsDefined 用于判断窗口是否已结束,High/Low 直接吐出区间边界。 CATRModule 单独持有 ATR 指标句柄。Init 里 iATR(_Symbol, TF, ATRPeriod) 建句柄,TF 是外部传的周期;Value 取最新 ATR 乘以 ATRMultiplier(默认 1.5),作为突破缓冲宽度;Release 在退出时释放句柄避免泄漏。 默认参数里 ATRPeriod=14、ATRMultiplier=1.5,箭头用 wingdings 233/234,邮件和推送默认关。外汇和贵金属波动大、杠杆高,实盘前务必在 MT5 策略测试器里用历史数据验一遍类的行为。 箭头和提醒参数先按默认跑,能让你专注看区间突破本身。
input class="type">int ATRPeriod = class="num">14; class=class="str">"cmt">// ATR look‑back input class="type">class="kw">double ATRMultiplier = class="num">1.5; class=class="str">"cmt">// ATR * multiplier class=class="str">"cmt">//--- arrows & alerts input class="type">int ArrowUpCode = class="num">233; class=class="str">"cmt">// wingdings ↑ input class="type">int ArrowDnCode = class="num">234; class=class="str">"cmt">// wingdings ↓ input class="type">class="kw">color ArrowUpColor = clrLime; class=class="str">"cmt">// class="type">long arrow input class="type">class="kw">color ArrowDnColor = clrRed; class=class="str">"cmt">// class="type">class="kw">short arrow input class="type">bool SendEmailAlert = class="kw">false; class=class="str">"cmt">// email on signal input class="type">bool PushNotify = class="kw">false; class=class="str">"cmt">// push on signal input class="type">class="kw">string EmailSubject = "ORB Signal"; class=class="str">"cmt">// mail subject class CRangeCapture { class="kw">private: class="type">class="kw">datetime startTime; class="type">class="kw">double hi, lo; class="kw">public: class="type">void Init(class="type">class="kw">datetime t, class="type">class="kw">double price) { startTime = t; hi = lo = price; } class="type">void Update(class="type">class="kw">double price, class="type">class="kw">datetime now) { if(now < startTime + RangeMinutes*class="num">60) { hi = MathMax(hi, price); lo = MathMin(lo, price); } } class="type">bool IsDefined(class="type">class="kw">datetime now) class="kw">const { class="kw">return(now >= startTime + RangeMinutes*class="num">60); } class="type">class="kw">double High() class="kw">const { class="kw">return hi; } class="type">class="kw">double Low() class="kw">const { class="kw">return lo; } }; class="kw">static CRangeCapture g_range; class CATRModule { class="kw">private: class="type">int handle; class="kw">public: class="type">bool Init() { handle = iATR(_Symbol, TF, ATRPeriod); class="kw">return(handle != INVALID_HANDLE); } class="type">class="kw">double Value() class="kw">const { class="type">class="kw">double buf[]; if(handle != INVALID_HANDLE && CopyBuffer(handle, class="num">0, class="num">0, class="num">1, buf) == class="num">1) class="kw">return buf[class="num">0] * ATRMultiplier; class="kw">return class="num">0.0; } class="type">void Release() { if(handle != INVALID_HANDLE) IndicatorRelease(handle); } };
◍ 回测确认与图表信息层的代码骨架
把突破和回踩逻辑封装成类之后,真正下单前还差两件事:确认回踩有效性、把信号可视化。下面这段静态对象定义和类结构可以直接贴进 MT5 的 EA 里跑,验证 ORB 策略的信号触发节奏。 CRetestSignal 类用 breakLong / breakShort / retested 三个布尔量记录状态。OnBreak 在收盘价超出区间高低点时置位,CheckRetest 则分两步:第一次触碰区间边界只标记 retested,不返信号;等收盘价重新站上(或跌破)边界才返回 true,避免假突破噪音。 CDashboard 类在左上角创建名为 ORB_Info 的 OBJ_LABEL,XDISTANCE 10、YDISTANCE 30 像素定位。Update 方法用 ObjectSetString 刷新文本,Delete 在退出时清理对象,防止图表残留。 信号触发后,StringFormat 把方向、入场价、止损、止盈拼成一行,例如 "LONG Signal @1.08542 SL=1.08410 TP=1.08790",再调 Alert 弹出。外汇与贵金属波动剧烈,这类信号仅代表概率倾向,实盘前务必用策略测试器按历史数据回测。
class="kw">static CATRModule g_atr; class CRetestSignal { class="kw">private: class="type">bool breakLong, breakShort, retested; class="kw">public: class="type">void Reset() { breakLong = breakShort = retested = class="kw">false; } class="type">void OnBreak(class="type">class="kw">double close, class="type">class="kw">double h, class="type">class="kw">double l) { breakLong = (close > h); breakShort = (close < l); retested = class="kw">false; } class="type">bool CheckRetest(class="kw">const class="type">MqlRates &r, class="type">bool &isLong) { if(breakLong) { if(!retested && r.low <= g_range.High()) { retested = true; isLong = true; class="kw">return class="kw">false; } if(retested && r.close > g_range.High()) { isLong = true; class="kw">return true; } } else if(breakShort) { if(!retested && r.high >= g_range.Low()) { retested = true; isLong = class="kw">false; class="kw">return class="kw">false; } if(retested && r.close < g_range.Low()) { isLong = class="kw">false; class="kw">return true; } } class="kw">return class="kw">false; } }; class="kw">static CRetestSignal g_retest; class CDashboard { class="kw">private: class="type">class="kw">string name; class="kw">public: class="type">void Init() { name = "ORB_Info"; if(ObjectFind(class="num">0, name) < class="num">0) { ObjectCreate(class="num">0, name, OBJ_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, class="num">30); } } class="type">void Update(class="kw">const class="type">class="kw">string &txt) { ObjectSetString(class="num">0, name, OBJPROP_TEXT, txt); } class="type">void Delete() { if(ObjectFind(class="num">0, name) >= class="num">0) ObjectDelete(class="num">0, name); } }; class="kw">static CDashboard g_dash; class="kw">static CTrade trade; class=class="str">"cmt">// ... inside your retest-confirmation block: class="type">class="kw">string msg = StringFormat("%s Signal @%.5f SL=%.5f TP=%.5f", isLong ? "LONG" : "SHORT", r[class="num">0].close, sl, tp); Alert(msg);