通用EA交易: CUnIndicator 和挂单的使用(第9部分)·进阶篇
⚙️

通用EA交易: CUnIndicator 和挂单的使用(第9部分)·进阶篇

(2/3)·还在为每个MT5指标手撸封装类?本篇用通用容器和挂单重载把重复劳动砍掉八成

新手友好 第 2/3 篇
不少交易者把第三篇的面向对象指标法当标准答案,真到要接几十个自定义指标时,光写封装类就耗光耐心。直接用系统函数裸调又让策略代码乱成一团,挂单还只能硬编码进业务逻辑。

「移动平均类里的取值与品种切换逻辑」

在 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 取单点。

MQL5 / C++
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 脚本验证。外汇和贵金属波动剧烈,这类底层验证虽不涉及下单,但句柄错乱会引发信号误判,属高风险环境操作。

MQL5 / C++
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]; 这一行。

MQL5 / C++
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 上,SetBuffer 的价值才显出来。UnMACD[1] 默认返回前一根柱的 MACD 主线值;若将 SIGNAL_LINE 设为默认缓冲,同一行 UnMACD[1] 返回的就变成信号线数值——内部缓冲切换对调用端完全透明。 除按索引外,也能按时间取数。代码里算过 time_span 等于前一根柱开盘时间,把它传给 UnMACD 而非索引 1,返回值和 UnMACD[1] 一致。外汇与贵金属市场波动剧烈、杠杆风险高,这类封装仅降低编码复杂度,不预示任何方向。

MQL5 / C++
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) 做一致性校验,控制台打印「从索引和时间上取得的数值相等」即说明两种访问等价。外汇与贵金属杠杆高,指标取值逻辑写错可能直接引发错单,建议先在策略测试器跑一遍确认。

MQL5 / C++
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 就能编译验证。

MQL5 / C++
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 验证最稳妥。

MQL5 / C++
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;
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到指标调用与挂单状态的实时梳理,你只需关注策略信号本身。

常见问题

前者是通用容器,靠 IndicatorCreate 接收任意指标句柄,不再需要为 iMA 或自定义指标单独写类;后者每个指标一套代码,维护成本陡增。
不会,交易引擎把挂单管理从策略代码剥离,重载后它与活动仓位走各自通道,概率上更稳,但外汇贵金属仍属高风险。
小布盯盘不替代 MT5 运行 EA,但可同步读取品种页的指标与挂单快照,帮你快速核对 CUnIndicator 输出的参数是否偏离预期。
多在品种未启动或参数越界时发生,建议在 CStrategy 初始化阶段做句柄非空校验,避免后续 GetData 拿到空序列。
标准加自定义指标几百个,每个写类工作量惊人,通用容器方案用 OOP 风格统一接入,倾向成为更实际的选择。