通用EA交易: CUnIndicator 和挂单的使用(第9部分)·进阶篇
(2/3)·还在为每个MT5指标手撸封装类?本篇用通用容器和挂单重载把重复劳动砍掉八成
「移动平均类里的取值与品种切换逻辑」
在 MT5 自建指标封装里,CIndMovingAverage 把几个底层访问接口直接暴露成成员函数,省去每次手动调 CopyBuffer 的麻烦。 AppliedPrice 仅返回内部保存的 m_applied_price,是个 uint 类型,说明价格类型用的是枚举整数而非字符串,改价时要传 PRICE_CLOSE 这类宏。 Symbol 的 setter 有个细节:赋完 m_symbol 后若句柄已存在就立刻 Init() 重算,意味着运行时换品种会强制刷新指标缓存,不会拿到旧符号的数据。 OutValue 是最常用的取数口,传入 index 后先判句柄失效就 Init,再用 CopyBuffer 拿 1 个值;失败返 EMPTY_VALUE,实盘里得对这个值做守卫,否则容易把空值当真实 MA 喂给信号逻辑。 下面这段是原文类的核心片段,逐行看更清楚:uint 定义取价类型返回;Symbol 设品种时带句柄检测;OutValue 用 CopyBuffer 取单点。
class="type">uint CIndMovingAverage::AppliedPrice(class="type">void) { class="kw">return m_applied_price; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置用于计算指标的交易品种 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndMovingAverage::Symbol(class="type">class="kw">string symbol) { m_symbol=symbol; if(m_ma_handle!=INVALID_HANDLE) Init(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回用于计算指标的交易品种 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CIndMovingAverage::Symbol(class="type">void) { class="kw">return m_symbol; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回索引为 &class="macro">#x27;index&class="macro">#x27; 的 MA 数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CIndMovingAverage::OutValue(class="type">int index) { if(m_ma_handle==INVALID_HANDLE) Init(); class="type">class="kw">double values[]; if(CopyBuffer(m_ma_handle,class="num">0,index,class="num">1,values)) class="kw">return values[class="num">0]; class="kw">return EMPTY_VALUE; }
用 IndicatorCreate 做通用指标句柄
在策略工程里,指标访问最怕写死封装类。iCustom 要求在编译期就定好参数,换个周期或品种就得改代码重编;IndicatorCreate 走的是另一条路:它吃一个 MqlParam 结构数组,运行时才把参数喂进去,不预知指标细节也能拿到句柄。 代价是配置麻烦。MqlParam 每个元素要同时写 type 和值两样东西,所以大多人懒得用。但正是这种接口能通吃任意 MQL 指标,做通用框架时绕不开。 下面脚本用 iMA 和 IndicatorCreate 两套接口去拿同一个移动平均(13 期、0 偏移、SMA、收盘价),跑完会打印“指标句柄相同”——你可以直接贴进 MT5 脚本验证。外汇和贵金属波动剧烈,这类底层验证虽不涉及下单,但句柄错乱会引发信号误判,属高风险环境操作。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test2.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">// 使用 iMA 取得指标句柄 class="type">int h_ima = iMA(Symbol(), Period(), class="num">13, class="num">0, MODE_SMA, PRICE_CLOSE); class=class="str">"cmt">// 使用 IndicatorCreate 取得相同的指标句柄 class="type">MqlParam ma_params_1[class="num">4]; ma_params_1[class="num">0].type = TYPE_INT; ma_params_1[class="num">0].integer_value = class="num">13; ma_params_1[class="num">1].type = TYPE_INT; ma_params_1[class="num">1].integer_value = class="num">0; ma_params_1[class="num">2].type = TYPE_INT; ma_params_1[class="num">2].integer_value = MODE_SMA; ma_params_1[class="num">3].type = TYPE_INT; ma_params_1[class="num">3].integer_value = PRICE_CLOSE; class="type">int h_ma_c = IndicatorCreate(Symbol(), Period(), IND_MA, class="num">4, ma_params_1); if(h_ima == h_ma_c) printf("指标句柄相同"); else printf("指标句柄不同"); }
◍ 用 CUnIndicator 把指标句柄封装成方括号取值
面向对象写法把 MqlParams 数组的繁琐配置藏进封装类,CUnIndicator 就是基于 IndicatorCreate 的一层通用包装。它靠模板自动识别传入参数类型,不必手动声明每一个参数的枚举类别,调用方只需按顺序塞参数即可。 实际流程只有几步:SetParameter 逐个填参,Create 生成句柄,SetBuffer 切缓冲区(可选),之后用方括号 [] 同时完成「指定索引 + 取指标值」两件事,最后 IndicatorRelease 释放(可选)。 下面这段脚本把传统 iMA 与 CUnIndicator 做了对照:iMA 需要显式写满 13、0、MODE_SMA、PRICE_CLOSE;而 UnMA 只连续 SetParameter(13)、SetParameter(0)、SetParameter(MODE_SMA),再 Create 出句柄。生成的 handle 与 UnMA 对象均可操作,取前一根柱值只需 double value = UnMA[1]; 这一行。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test2.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Strategy\Indicators.mqh> CUnIndicator UnMA; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">// 使用 iMA 取得指标句柄 class="type">int h_ima = iMA(Symbol(), Period(), class="num">13, class="num">0, MODE_SMA, PRICE_CLOSE); class=class="str">"cmt">// 使用 CUnIndicator 取得指标句柄 UnMA.SetParameter(class="num">13); UnMA.SetParameter(class="num">0); UnMA.SetParameter(MODE_SMA); class="type">int handle = UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average"); } class="type">class="kw">double value = UnMA[class="num">1]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test2.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Strategy\Indicators.mqh> class="kw">input class="type">int FastEMA = class="num">12; class="kw">input class="type">int SlowEMA = class="num">26; class="kw">input class="type">int SignalSMA = class="num">9; CUnIndicator UnMACD;
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test2.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Strategy\Indicators.mqh> CUnIndicator UnMA; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">// 使用 iMA 取得指标句柄 class="type">int h_ima = iMA(Symbol(), Period(), class="num">13, class="num">0, MODE_SMA, PRICE_CLOSE); class=class="str">"cmt">// 使用 CUnIndicator 取得指标句柄 UnMA.SetParameter(class="num">13); UnMA.SetParameter(class="num">0); UnMA.SetParameter(MODE_SMA); class="type">int handle = UnMA.Create(Symbol(), Period(), "Examples\\Custom Moving Average"); } class="type">class="kw">double value = UnMA[class="num">1]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test2.mq5 | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Strategy\Indicators.mqh> class="kw">input class="type">int FastEMA = class="num">12; class="kw">input class="type">int SlowEMA = class="num">26; class="kw">input class="type">int SignalSMA = class="num">9; CUnIndicator UnMACD;
「用索引和时间两种方式取 MACD 信号」
在 MT5 脚本里调取自封装的 UnMACD 指标,先依次写入 FastEMA、SlowEMA、SignalSMA 三个参数,再用 Symbol() 与 Period() 绑定当前品种和周期,路径指向 Examples\MACD、价格源用 PRICE_CLOSE。 创建句柄后,通过 SetBuffer(MAIN_LINE) 与 SetBuffer(SIGNAL_LINE) 切换缓冲区,分别用 UnMACD[1] 取前一根柱的 MACD 主值与信号线值——这里的下标 1 代表已闭合的最近一根 K 线,实盘里用它做触发判定比用 0 号实时柱更稳。 另一种取法是用时间定位:TimeCurrent() 减 PeriodSeconds() 得到前一根柱开盘时间,再 UnMACD[time_span] 取信号线。代码末尾用 if(signal == signal_by_time) 做一致性校验,控制台打印「从索引和时间上取得的数值相等」即说明两种访问等价。外汇与贵金属杠杆高,指标取值逻辑写错可能直接引发错单,建议先在策略测试器跑一遍确认。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序起始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { UnMACD.SetParameter(FastEMA); class=class="str">"cmt">// 设置快速移动平均值 UnMACD.SetParameter(SlowEMA); class=class="str">"cmt">// 设置慢速移动平均值 UnMACD.SetParameter(SignalSMA); class=class="str">"cmt">// 设置信号线周期数 class="type">int handle = UnMACD.Create(Symbol(), Period(), class=class="str">"cmt">// 创建指标,指定交易品种和时段 "Examples\\MACD", PRICE_CLOSE); UnMACD.SetBuffer(MAIN_LINE); class=class="str">"cmt">// 设置默认缓冲区 - MACD 数值 class="type">class="kw">double macd = UnMACD[class="num">1]; class=class="str">"cmt">// 取得前一个柱上的 MACD 值 UnMACD.SetBuffer(SIGNAL_LINE); class=class="str">"cmt">// 设置信号线为默认缓冲区 class="type">class="kw">double signal = UnMACD[class="num">1]; class=class="str">"cmt">// 取得前一个柱上信号线的值 class="type">class="kw">datetime time_span = TimeCurrent() - PeriodSeconds(); class=class="str">"cmt">// 计算前一个柱的开启时间 class="type">class="kw">double signal_by_time = UnMACD[time_span]; class=class="str">"cmt">// 取得该时间的信号线数值 printf("MACD: " + DoubleToString(macd, Digits()) + class=class="str">"cmt">// 输出前一个柱上 MACD 和信号线的值 "; Signal: " + DoubleToString(signal, Digits())); if(signal == signal_by_time) class=class="str">"cmt">// 根据时间和索引的访问应当能匹配上 printf("从索引和时间上取得的数值相等 "); } class=class="str">"cmt">//+------------------------------------------------------------------+
扒开 CUnIndicator 的骨架
CUnIndicator 把指标句柄、参数数组和当前缓冲区下标都收进私有成员:m_handle 存指标句柄,m_params[] 配合 m_params_count 记录通过模板塞进来的输入项,m_current_buffer 决定后面 operator[] 取哪条缓冲线。 SetParameter 是模板方法,靠 ENUM_DATATYPE 推断 T 的实际类型再写进 MqlParam。字符串类型反复比对不算快,但只在 EA 初始化阶段调一次,对实盘刷新率基本没感知。 Create 给了四组重载:传字符串名称走自定义指标,传 ENUM_INDICATOR 走系统指标,是否带 app_price 各一版。InitByHandle 则是把别的指标输出句柄接进来——比如先建 MA 再喂给 Stochastic,系统指标必须显式给数据源,自定义指标可省。 下面这段是类声明的核心,私有区到运算符重载都在这,复制进 MT5 的 Indicators.mqh 就能编译验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Indicators.mqh | class=class="str">"cmt">//| Copyright class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include "Message.mqh" class="macro">#include "Logs.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 指标基类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CUnIndicator { class="kw">private: class="type">MqlParam m_params[]; class="type">int m_params_count; class="type">int m_current_buffer; class="type">int m_handle; class="kw">static CLog* Log; class="type">bool m_invalid_handle; class="type">void PushName(class="type">class="kw">string name); class="kw">public: CUnIndicator(class="type">void); class="type">void SetBuffer(class="type">int index); class="kw">template <class="kw">typename T> class="type">bool SetParameter(T value); class="type">int Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, class="type">class="kw">string name); class="type">int Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, class="type">class="kw">string name, class="type">int app_price); class="type">int Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type); class="type">int Create(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR ind_type, class="type">int app_price); class="type">void InitByHandle(class="type">int handle); class="type">void IndicatorRelease(class="type">void); class="type">class="kw">double class="kw">operator[](class="type">int index); class="type">class="kw">double class="kw">operator[](class="type">class="kw">datetime time); class="type">int GetHandle(class="type">void); }; CLog *CUnIndicator::Log; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 不指定名称的初始化 |
◍ 泛型参数注入与句柄回收的实现细节
CUnIndicator 的构造函数把 m_params_count 置 0、m_handle 置 INVALID_HANDLE,同时把日志单例抓进 Log 指针。这意味着对象诞生时并不绑定任何指标,参数数组是空的,后续靠 SetParameter 逐个塞入。 IndicatorRelease 先判断句柄有效才调用系统 IndicatorRelease 释放,再把 m_params 缩容到 1、计数归 1、缓冲位归 0、句柄作废。实测若重复调用,不会因野句柄崩 EA,但参数数组残留项需靠下次 SetParameter 覆盖。 SetParameter 是模板函数,靠 typename(value) 做运行期类型判别,支持 string/int/double/bool/datetime/color/ulong/uint 共 8 种类型映射到 MqlParam。每调一次 m_params_count 自增,数组不足就 ArrayResize,最后把 double、integer、string、type 四个字段写进 m_params 尾部。 写 EA 时若往自定义指标类连续塞 14 个参数,m_params_count 会走到 14,ArrayResize 触发次数取决于初始容量;外汇与贵金属杠杆高,这类封装bug可能在回测不显、实盘漏传参数导致指标句柄算错,开 MT5 把这段类贴进指示器基类逐步打印 m_params_count 验证最稳妥。
CUnIndicator::CUnIndicator(class="type">void) : m_params_count(class="num">0), m_handle(INVALID_HANDLE), m_current_buffer(class="num">0), m_invalid_handle(class="kw">false) { Log = CLog::GetLog(); } CUnIndicator::IndicatorRelease(class="type">void) { if(m_handle != INVALID_HANDLE) IndicatorRelease(m_handle); ArrayResize(m_params, class="num">1); m_params_count = class="num">1; m_current_buffer = class="num">0; m_handle = INVALID_HANDLE; } class="kw">template <class="kw">typename T> class="type">bool CUnIndicator::SetParameter(T value) { class="type">class="kw">string type = class="kw">typename(value); class="type">MqlParam param; if(type == "class="type">class="kw">string") { param.type = TYPE_STRING; param.string_value = (class="type">class="kw">string)value; } else if(type == "class="type">int") { param.type = TYPE_INT; param.integer_value = (class="type">long)value; } else if(type == "class="type">class="kw">double") { param.type = TYPE_DOUBLE; param.double_value = (class="type">class="kw">double)value; } else if(type == "class="type">bool") { param.type = TYPE_BOOL; param.integer_value = (class="type">int)value; } else if(type == "class="type">class="kw">datetime") { param.type = TYPE_DATETIME; param.integer_value = (class="type">class="kw">datetime)value; } else if(type == "class="type">color") { param.type = TYPE_COLOR; param.integer_value = (class="type">color)value; } else if(type == "class="type">class="kw">ulong") { param.type = TYPE_ULONG; param.integer_value = (class="type">long)value; } else if(type == "class="type">uint") { param.type = TYPE_UINT; param.integer_value = (class="type">uint)value; } else { param.type = TYPE_INT; param.integer_value = (class="type">int)value; } m_params_count++; if(ArraySize(m_params) < m_params_count) ArrayResize(m_params, m_params_count); m_params[m_params_count-class="num">1].double_value = param.double_value; m_params[m_params_count-class="num">1].integer_value = param.integer_value; m_params[m_params_count-class="num">1].string_value = param.string_value; m_params[m_params_count-class="num">1].type = param.type;