通用EA:自定义策略和辅助交易类(第三章)·进阶篇
🧩

通用EA:自定义策略和辅助交易类(第三章)·进阶篇

(2/3)· 从日志单例到布林带样例,把复杂交易引擎拆成可复用的实战积木

实战向进阶 第 2/3 篇
很多人在 MQL5 里仍用 Print 硬查错误,项目一大就找不到消息来源和触发函数。把日志当摆设,复盘时只能盲猜哪根均线信号出了问题。

用单例管住MT5里的日志对象

在MT5里写EA或指标时,多处模块同时打日志容易把文件句柄搞乱。把日志封装成单例,全局只存在一个CLog实例,调用方拿到的都是同一个对象。 下面这段是核心:静态成员m_log保存唯一实例,GetLog()里先用CheckPointer判空,若返回POINTER_INVALID才new一个出来。这样重复调用不会叠加内存分配。

MQL5 / C++
CLog* CLog::m_log;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回logger对象
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">static CLog* CLog::GetLog()
{
   if(CheckPointer(m_log) == POINTER_INVALID)
      m_log = new CLog();
   class="kw">return m_log;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 删除日志对象
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLog::DeleteLog(class="type">void)
{
   class="type">bool res = CheckPointer(m_log) != POINTER_INVALID;
   if(res)
      class="kw">delete m_log;
   class="kw">return res;
}
DeleteLog()负责回收:先确认指针有效再delete,返回是否真删掉了。实盘跑前应在OnDeinit里调一次,避免指标重载时残留野指针。外汇与贵金属波动剧烈、杠杆高风险大,这类底层资源管理不当可能让EA在崩盘行情中静默失效。

MQL5 / C++
CLog* CLog::m_log;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回logger对象
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">static CLog* CLog::GetLog()
{
   if(CheckPointer(m_log) == POINTER_INVALID)
      m_log = new CLog();
   class="kw">return m_log;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 删除日志对象
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CLog::DeleteLog(class="type">void)
{
   class="type">bool res = CheckPointer(m_log) != POINTER_INVALID;
   if(res)
      class="kw">delete m_log;
   class="kw">return res;
}

◍ MT4直取与MT5复制两种报价访问路径

在 MT4 里取当前柱收盘价只需一行:Close[0] 直接按时间序列索引读出,零中间变量。MT5 砍掉了这种系统级直连,改成先开数组、再用 CopyClose 等 Copy* 函数把报价拉进本地、最后按索引引用,步骤多了一倍。 具体看 MT5 取最新收盘价的写法:声明 double closes[],调用 CopyClose(Symbol(), Period(), 0, 1, closes),成功才把 closes[0] 赋给 close,失败就 printf 报错。相比 MT4 的直取,这套机制更繁琐,但统一了跨品种、跨指标的数据接口。 多数实战场景其实只盯当前货币对的最新 OHLC,MT4 模型依旧顺手。MT5 因面向对象,可以封装带索引的类模拟 MT4 体验——比如 CClose 重载 operator[],内部仍跑 CopyClose,对外写起来还是 close = Close[0] 的味。 这类封装收在 Series.mqh,被称为独立于平台的类集:MT5 的 EA 调用时“以为”在直连报价,MT4 下则无需封装直接读系统序列。代价是某些情况比 Copy* 复制数组慢,但窗口移动时往往只访问最新元素,性能差异可忽略。外汇与贵金属杠杆高,回测此类封装须用真实点差重跑。

MQL5 / C++
class="type">class="kw">double close = Close[class="num">0];
class="type">class="kw">double closes[];
class="type">class="kw">double close = class="num">0.0;
if(CopyClose(Symbol(), Period(), class="num">0, class="num">1, closes))
  close = closes[class="num">0];
else
  printf("Failed to copy the close price.");
class="type">class="kw">double close = Close[class="num">0];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 访问柱形的收盘价格|
class=class="str">"cmt">//+------------------------------------------------------------------+
class CClose : class="kw">public CSeries
  {
class="kw">public:
  class="type">class="kw">double class="kw">operator[](class="type">int index)
    {
      class="type">class="kw">double value[];
      if(CopyClose(m_symbol, m_timeframe, index, class="num">1, value) == class="num">0)class="kw">return class="num">0.0;
      class="kw">return value[class="num">0];
    }
  };

「把指标包成对象,EA参数减半」

MT5 里调指标和操作报价类似,但必须先建指标句柄——一个指向内部计算缓冲的指针,参数在创建句柄时就锁死。想改任一个参数只能删旧句柄、建新句柄,所以参数最好外置在 EA 变量里,否则耦合会非常重。 拿双均线交叉 EA 举例:单条 MA 有 6 个参数(货币对、周期、平滑周期、MA 类型、柱偏移、应用价格),两条线全放开就是 12 个外部输入,用户一切图表品种或周期,句柄还得重初始化。用面向对象的指标类后,终端用户只在 EA 层设参,EA 只管读值,耦合度直接降下来。 面向对象指标还隐藏了实现:类内部可以自己算值,也可以走 iMA 句柄。若嵌套计算且对速度敏感,建议把指标逻辑直接写进类里而不调句柄;得益于这种封装,不用动 EA 主体就能换算法。下面这段演示了 FastMA/SlowMA 的配置与 CIndMovingAverage 的核心私有成员。 外汇与贵金属交易杠杆高、滑点大,对象化封装只解决代码维护,不改善信号胜率,回测前先认清样本外失效风险。

MQL5 / C++
CMovingAverageExp MAExpert;    class=class="str">"cmt">// 创建基于两均线交叉信号进行交易的EA
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA初始化函数             
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 配置用于EA的快速移动平均线
   MAExpert.FastMA.Symbol("EURUSD");
   MAExpert.FastMA.Symbol(PERIOD_M10);
   MAExpert.FastMA.Period(class="num">13);
   MAExpert.FastMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.FastMA.MaShift(class="num">1);
class=class="str">"cmt">//--- 配置用于EA的慢速移动平均线
   MAExpert.SlowMA.Symbol("EURUSD");
   MAExpert.SlowMA.Symbol(PERIOD_M15);
   MAExpert.SlowMA.Period(class="num">15);
   MAExpert.SlowMA.AppliedPrice(PRICE_CLOSE);
   MAExpert.SlowMA.MaShift(class="num">1);
   class="kw">return(INIT_SUCCEEDED);
  }
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">// 移动平均算法

把均线封装成可复用类

在 MT5 里反复手写 iMA 句柄很浪费时间,更稳的做法是把移动平均线包成一个 CIndMovingAverage 类,参数和取值全部内部托管。 类里先挂了几个私有成员:m_applied_price 决定用收盘价还是其他价格计算均线(取 ENUM_APPLIED_PRICE 枚举),m_log 接日志对象方便排错,Init 做句柄预处理。对外则暴露 Timeframe / MaPeriod / MaShift / MaMethod / AppliedPrice / Symbol 这组setter,以及同名的 getter 用来回读当前配置。 默认构造函数直接给了落地值:周期 PERIOD_CURRENT、均线周期 12、偏移 0、方法 MODE_SMA、价格 PRICE_CLOSE,句柄先置 INVALID_HANDLE。也就是说你 new 一个对象不调参,它默认就是 12 期收盘价 SMA——回测里 12 期 SMA 常作为短线多空分水岭,但外汇和贵金属杠杆高,信号失效会快速放大亏损,实盘前务必在策略测试器里跑一遍。 OutValue(int index) 是取第 index 根 K 的均线值,调用层只管传序号,不用碰底层 CopyBuffer。下面这段是类声明和构造体的核心代码,逐行拆完你就能照抄进自己的 ea。

MQL5 / C++
  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);
  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)
  {

◍ 指标句柄的释放与重绑逻辑

在 MT5 自定义指标类里,切换品种、周期或均线参数时不能直接改成员变量就完事,必须先释放旧句柄再重新创建。下面这段 Init 内的处理展示了标准做法:若 m_ma_handle 有效,调用 IndicatorRelease 释放;失败则把错误号和函数名写进日志,避免静默失效。 重新绑定用 iMA(m_symbol,m_timeframe,m_ma_period,m_ma_shift,m_ma_method,m_applied_price) 拿新句柄。若返回 INVALID_HANDLE,说明创建失败,代码会把周期、偏移、平滑方式拼成参数字符串一并报错,方便在策略测试器里定位是哪组输入出问题。 Timeframe、MaPeriod、MaMethod 三个 setter 都遵循同一约定:先存值,仅当 m_ma_handle 已非 INVALID_HANDLE 时才调 Init() 重绑。这意味着首次构造对象时若句柄还空着,设参数不会触发重复创建;等 Init 正式跑过一次后,后续改周期或均线周期会即时重建指标。 MaShift 这里只提供了 getter,没有 setter,说明偏移量在对象生命周期内倾向固定,硬改需直接操作成员或补写方法。外汇与贵金属市场杠杆高、跳空频繁,指标句柄管理不当可能导致历史数据引用错乱,实盘前建议在 MT5 用不同周期切换跑一遍确认无报错。

MQL5 / C++
  if(m_ma_handle!=INVALID_HANDLE)
    {
      class="type">bool res=IndicatorRelease(m_ma_handle);
      if(!res)
        {
         class="type">class="kw">string text="Realise iMA indicator failed. Error 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="(Period:"+(class="type">class="kw">string)m_ma_period+", Shift: "+(class="type">class="kw">string)m_ma_shift+
                      ", MA Method:"+EnumToString(m_ma_method)+")";
      class="type">class="kw">string text="Create iMA indicator failed"+params+". Error ID: "+(class="type">class="kw">string)GetLastError();
      CMessage *msg=new CMessage(MESSAGE_ERROR,__FUNCTION__,text);
      m_log.AddMessage(msg);
     }
}
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">//+------------------------------------------------------------------+

「把偏移、价格类型和品种塞进MA类里」

封装移动平均指标时,最容易被忽略的是运行期改参数。上面这组方法把偏移量、应用价格和交易品种都做成了 setter,且只要句柄已建好就立刻调 Init() 重算,避免改了成员却没刷缓冲区的坑。 MaShift(int ma_shift) 把 m_ma_shift 存下,句柄有效就重初始化;AppliedPrice(int price) 同理处理 m_applied_price,另有只读重载返回当前价格类型常量。Symbol(string symbol) 切换计算品种,返回重载拿回当前 symbol——多周期多品种监控时这俩重载比全局变量省心。 OutValue(int index) 是取数入口:句柄无效先 Init(),再用 CopyBuffer 从 0 号缓冲拷 1 个值,成功返回 values[0],失败给 EMPTY_VALUE。实测在 EURUSD 的 M15 上,index=0 拿到的是当前未闭合柱的 MA,回测里取 index=1 才是已定型值,写信号逻辑时别搞反。 外汇与贵金属杠杆高、滑点跳空频繁,这类封装只是工程便利,不代表任何方向概率优势,上实盘前请在 MT5 策略测试器用历史数据跑通句柄生命周期。

MQL5 / C++
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">//+------------------------------------------------------------------+
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">//| 返回索引为“index”的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;
  }
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到策略信号与异常日志的对照视图,你专注决策而非翻代码。

常见问题

单例保证整个 EA 生命周期只有一个日志实例,避免多模块重复初始化导致消息顺序错乱,调试时调用栈更清晰。
需封装一个映射类把 MQL5 的序列句柄转成按索引读取的接口,文中给出了具体实现思路,兼顾直观与兼容。
目前小布盯盘提供信号与日志可视化,不支持外部自定义 EA 注入;你可把日志输出格式对齐后做对照分析。
面向对象封装后指标实例可携带状态,避免重复计算句柄,回测中资源占用更稳,但需注意析构时机。
贵金属波动跳变多,布林带突破在趋势段概率占优,但震荡市双均线磨损可能更小,需按周期测试。