通用EA交易: CUnIndicator 和挂单的使用(第9部分)(基础篇)
📘

通用EA交易: CUnIndicator 和挂单的使用(第9部分)(基础篇)

第 1/3 篇

「用 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 写专用类,在策略频繁换指标时纯属负收益。直接调系统函数,改参数也就是换几个实参的事。

MQL5 / C++
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 方法,方便你直接在专家日志里定位是哪组参数出的问题。外汇和贵金属波动大,这类封装只是降低代码耦合,不预示任何方向,实盘前请在策略测试器里跑一遍。

MQL5 / C++
  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 数据确认信号偏移幅度。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

把均线参数做成可热改接口,运行时从外部输入读取周期,改完直接生效,不必重新编译EA。
旧版每个指标都要手写句柄获取、缓冲读取和释放,CUnIndicator把这些都收进基类,子类只填参数。
小布可以读取你的EA结构描述,指出挂单类和指标封装的冗余点,并给出可复用类的重构建议。
核心是统一维护挂单 Ticket、类型与价格映射,在Tick里做存在性校验和超时撤单,封装后只需调方法。
不会,只要每个实例独立传周期参数并各自持有句柄,多周期并行读取缓冲互不干扰。