通用EA交易: CUnIndicator 和挂单的使用(第9部分)(基础篇)
「用 CUnIndicator 管理挂单的底层思路」
在 MT5 的通用 EA 框架里,CUnIndicator 负责把指标缓冲区抽象成可调用对象,避免每次都重写 iCustom 句柄与索引定位。把它和挂单逻辑放在一起,核心好处是:建仓前就能用指标状态过滤挂单方向,而不是成交后才判断。 原文示例发布于 2017-10-23,历史浏览 8089、评论 32,说明这类把指标封装成类再配合挂单管理的写法在早期 MQL5 社区已有明确需求。你可以在 MT5 里新建 EA,把指标调用改成 CUnIndicator 成员,观察挂单触发频率是否下降。 外汇与贵金属杠杆品种波动剧烈,用指标过滤挂单只降低无效触发概率,不消除滑点与黑天鹅风险,参数需先在策略测试器用历史数据验证。
这套指标封装到底改了什么
早期 CStrategy 框架里,策略类直接调用指标句柄,每接一个新指标就要重写一堆访问逻辑,维护成本随指标数量线性膨胀。 新结构把指标访问收敛到 CUnIndicator 这一层,用 IndicatorCreate 做统一入口,策略侧只关心信号,不关心底层句柄怎么拿。 从目录能看出,本次重构覆盖了从基础函数、内部结构到挂单优化和 CImpulse 2.0 策略落地的完整链路,属于框架级而非单策略修补。 想在 MT5 里验证改动幅度,直接对比旧版 CStrategy 的 OnTick 指标调用行数和新版 CUnIndicator 封装后的调用行数,差距通常在一倍以上。
◍ 指标封装与挂单管理的重构思路
早期 CStrategy 交易引擎把大部分方法都整合进通用 EA 框架,实盘里确实省事,但指标操作这块一直别扭。系列第三篇文章曾给每个指标写独立 OOP 封装类,思路是「一个指标一个类、自带取数方法」,落地后才发现:MT5 自带加自定义指标少说几十种,逐个写类成本太高,复用也差。 本文给出一条更轻的路子:用 OOP 风格操作指标,但不再要求给每个指标实现单独模块类。核心是把指标访问抽象成统一接口,调用方只关心缓冲区与句柄,不关心底层是哪个 iMACD 还是 iCustom,改写策略时少掉一大半样板代码。 第二个改动在挂单。以前要在策略代码里直接管 pending order,现在这部分函数挪进了 CStrategy 引擎。最终策略类只要重载 SupportBuyPending 和 SupportSellPending 两个方法,就能像管活动仓位一样管挂单,逻辑收口到引擎层,策略文件更干净。 外汇与贵金属杠杆高、滑点跳空频繁,挂单管理改到引擎层后,仍建议在 MT5 策略测试器里用真实点差回测一遍再上模拟盘。
「旧版封装类写指标有多累」
早先的 CStrategy 思路是给每个指标单独写封装类,比如操作 iMA 就用 CIndMovingAverage。这个类里塞满了句柄、周期、偏移、价格类型等私有成员,外加一堆 setter 和 Init 调用系统函数,光一个最普通的均线封装就长达数百行。 MetaTrader 生态里指标数以百计,标准的、自定义的各有各的参数集合。按那种写法,每一个都得手搓一个独立类,工作量直接爆炸,而且维护起来极易漏改。 实际写策略时,我更倾向直接在交易逻辑里调 iMA 这类系统函数,而不是先花半天造轮子。下面这段就是当年那个均线封装类的头文件片段,可以直观感受一下‘简单指标’的代码密度。 别把封装当银弹 为单个 MA 写专用类,在策略频繁换指标时纯属负收益。直接调系统函数,改参数也就是换几个实参的事。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MovingAverage.mqh | class=class="str">"cmt">//| Copyright class="num">2015, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Strategy\Message.mqh> class="macro">#include <Strategy\Logs.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定义 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndMovingAverage { class="kw">private: class="type">int m_ma_handle; class=class="str">"cmt">// 指标句柄 ENUM_TIMEFRAMES m_timeframe; class=class="str">"cmt">// 时段 class="type">int m_ma_period; class=class="str">"cmt">// 周期数 class="type">int m_ma_shift; class=class="str">"cmt">// 偏移 class="type">class="kw">string m_symbol; class=class="str">"cmt">// 交易品种 ENUM_MA_METHOD m_ma_method; class=class="str">"cmt">// 移动平均方法 class="type">uint m_applied_price; class=class="str">"cmt">// 指标句柄,您会用它计算移动平均数值, class=class="str">"cmt">// 或者是 ENUM_APPLIED_PRICE 类型的价格值 CLog* m_log; class=class="str">"cmt">// Logging class="type">void Init(class="type">void); class="kw">public: CIndMovingAverage(class="type">void); class=class="str">"cmt">/*参数*/ class="type">void Timeframe(ENUM_TIMEFRAMES timeframe); class="type">void MaPeriod(class="type">int ma_period); class="type">void MaShift(class="type">int ma_shift); class="type">void MaMethod(ENUM_MA_METHOD method); class="type">void AppliedPrice(class="type">int source);
把均线指标封装成可复用类
在 MT5 里写 EA 或脚本,反复调用 iMA 容易把句柄管理弄乱。把移动均线包成一个 CIndMovingAverage 类,初始化时统一拿句柄、统一报错,后面取数只调 OutValue(index) 就行。 类里先声明一组取值接口:Symbol() 返回品种名,Timeframe() 返回周期枚举,MaPeriod()、MaShift() 给周期数和偏移量,MaMethod() 是平滑方式,AppliedPrice() 是应用价格,OutValue(int index) 按序号吐出指标值。 默认构造函数把参数钉死了一套保守值:周期 PERIOD_CURRENT、均线长度 12、偏移 0、方法 MODE_SMA、价格用 PRICE_CLOSE。也就是说不传参时,它默认就是一张 12 期收盘价的简单均线。 Init() 里先判断旧句柄是否有效,有效就 IndicatorRelease 释放掉再重建,避免重复挂指标吃内存。重建失败会拼一段带参数的错误文本写进日志,包含周期数、偏移和 MA 方法,方便你直接在专家日志里定位是哪组参数出的问题。外汇和贵金属波动大,这类封装只是降低代码耦合,不预示任何方向,实盘前请在策略测试器里跑一遍。
class="type">void Symbol(class="type">class="kw">string symbol); ENUM_TIMEFRAMES Timeframe(class="type">void); class="type">int MaPeriod(class="type">void); class="type">int MaShift(class="type">void); ENUM_MA_METHOD MaMethod(class="type">void); class="type">uint AppliedPrice(class="type">void); class="type">class="kw">string Symbol(class="type">void); class=class="str">"cmt">/*输出值*/ class="type">class="kw">double OutValue(class="type">int index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 默认构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CIndMovingAverage::CIndMovingAverage(class="type">void) : m_ma_handle(INVALID_HANDLE), m_timeframe(PERIOD_CURRENT), m_ma_period(class="num">12), m_ma_shift(class="num">0), m_ma_method(MODE_SMA), m_applied_price(PRICE_CLOSE) { m_log=CLog::GetLog(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 初始化 | class=class="str">"cmt">//+------------------------------------------------------------------+ CIndMovingAverage::Init(class="type">void) { if(m_ma_handle!=INVALID_HANDLE) { class="type">bool res=IndicatorRelease(m_ma_handle); if(!res) { class="type">class="kw">string text="初始化 iMA 指标失败,错误 ID: "+(class="type">class="kw">string)GetLastError(); CMessage *msg=new CMessage(MESSAGE_WARNING,__FUNCTION__,text); m_log.AddMessage(msg); } } m_ma_handle=iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price); if(m_ma_handle==INVALID_HANDLE) { class="type">class="kw">string params="(周期数:"+(class="type">class="kw">string)m_ma_period+", 偏移: "+(class="type">class="kw">string)m_ma_shift+ ", MA 方法:"+EnumToString(m_ma_method)+")"; class="type">class="kw">string text="创建 iMA 指标失败"+params+". Error ID: "+(class="type">class="kw">string)GetLastError(); CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text); m_log.AddMessage(msg); } }
◍ 把均线参数做成可热改的接口
在 MT5 里自己封装均线类时,最怕参数写死在构造函数里,调一次周期就得重新编译。下面这组 setter/getter 把时段、周期、算法、偏移、价格类型全部暴露成成员函数,句柄有效时改任意一项都会自动调 Init() 重建指标。 void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf) { m_timeframe=tf; if(m_ma_handle!=INVALID_HANDLE) Init(); } 这截里,m_timeframe 存目标周期;只有 m_ma_handle 不是 INVALID_HANDLE(即指标已创建)才跑 Init(),避免空句柄崩溃。 ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(void) { return m_timeframe; } 对应的 getter 直接返回当前周期,不带参数重载,编译器靠签名区分读写版本。 void CIndMovingAverage::MaPeriod(int ma_period) { m_ma_period=ma_period; if(m_ma_handle!=INVALID_HANDLE) Init(); } int CIndMovingAverage::MaPeriod(void) { return m_ma_period; } 周期数用 int 传,改完同样触发 Init();实盘里把 14 改成 20 不用停 EA,下一根 K 线即按新周期算。 void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method) { m_ma_method=method; if(m_ma_handle!=INVALID_HANDLE) Init(); } ENUM_MA_METHOD CIndMovingAverage::MaMethod(void) { return m_ma_method; } 算法枚举决定 SMA/EMA 等,换方法也会重建缓冲。 int CIndMovingAverage::MaShift(void) { return m_ma_shift; } void CIndMovingAverage::MaShift(int ma_shift) { m_ma_shift=ma_shift; if(m_ma_handle!=INVALID_HANDLE) Init(); } 偏移 getter 先写、setter 后写,逻辑对称;偏移常用于把均线右移看信号滞后。 void CIndMovingAverage::AppliedPrice(int price) { m_applied_price = price; if(m_ma_handle != INVALID_HANDLE) Init(); } 价格类型用 int 承接(PRICE_CLOSE 等宏本质就是整型),切到 PRICE_OPEN 算均线同样即时生效。外汇与贵金属杠杆高,参数热改后建议先在策略测试器跑 3 个月 tick 数据确认信号偏移幅度。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置时段 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::Timeframe(ENUM_TIMEFRAMES tf) { m_timeframe=tf; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回当前时段 | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES CIndMovingAverage::Timeframe(class="type">void) { class="kw">return m_timeframe; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置移动平均的平均周期数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::MaPeriod(class="type">int ma_period) { m_ma_period=ma_period; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回移动平均的当前平均周期数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CIndMovingAverage::MaPeriod(class="type">void) { class="kw">return m_ma_period; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置移动平均类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::MaMethod(ENUM_MA_METHOD method) { m_ma_method=method; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回移动平均类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_MA_METHOD CIndMovingAverage::MaMethod(class="type">void) { class="kw">return m_ma_method; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回移动平均偏移 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CIndMovingAverage::MaShift(class="type">void) { class="kw">return m_ma_shift; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置移动平均偏移 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::MaShift(class="type">int ma_shift) { m_ma_shift=ma_shift; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置用于计算 MA 的价格类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::AppliedPrice(class="type">int price) { m_applied_price = price; if(m_ma_handle != INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回用于 MA 计算的价格类型 | class=class="str">"cmt">//+------------------------------------------------------------------+