通用EA:自定义策略和辅助交易类(第三章)(基础篇)
📘

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

第 1/3 篇

通用EA的自定义策略与辅助类起点

在 MT5 里写通用 EA,核心是把「策略逻辑」和「下单/风控等辅助动作」拆成不同类,避免把所有代码塞进一个 OnTick。这样后续换策略只改策略类,辅助类(如仓位计算、滑点控制)可复用。 原文示例由 Vasiliy Sokolov 于 2016 年 5 月 17 日发布,累计浏览 6242 次、评论 19 条,说明这类模块化思路在 MQL5 社区早期就有明确需求。 对外汇与贵金属交易者而言,这种结构能降低改参数试错成本,但杠杆品种波动剧烈,回测通过不代表实盘概率占优,需先在策略测试器跑历史数据。

「这份材料会带你过哪些点」

这一节先把后续要展开的技术地图列清楚,方便你对照 MT5 环境逐个验证。 内容覆盖从基础的日志系统讲起,包括 CMessage 与 CLog 类的用法,以及单例模式在 MQL5 里的落地方式。 接着会用到 MetaTrader 4 的索引方式去访问报价数据,再把指标封装成面向对象的形式来调用。 EA 重载方法是重点之一,后面用双移动平均线信号和布林带突破两个样例分别跑一遍逻辑。 最后是把自定义策略接进交易引擎,整套材料共 6 节,本节是第 2 节,属于目录层。外汇与贵金属交易杠杆高,任何代码样例都先在策略测试器跑历史数据再上实盘。

◍ CStrategy 引擎样例与单例日志

本篇承接前两篇:第一篇讲过按时间与星期几切换 EA 逻辑的交易模式,第二篇拆过集中事件处理模型和 CStrategy 基类主干。这一节先给后续要落地的样例定调——基于 CStrategy 引擎加若干辅助算法写一套可运行的 EA。 重点提一个容易被忽略的件:日志记录器。它只做支撑活,但在复杂系统里缺了就难排错。样例里的日志器用单例模式写,全进程只有一个实例,调起来不用来回传引用,对做非标准任务的算法同样有用。 另一块是市场数据访问。MT4 里习惯写 Close[1]、High[0] 这种带索引的写法,MT5 原生不这么来。本文给出一套映射算法,让你在 MT5 里也能用直观索引抓柱面数据,复制过去就能在策略里直接调。 CStrategy 整套算法集合不浅,但用面向对象那套思路,你不必啃透内部机制。哪段看不懂直接跳过,只要知道引擎对外能干啥就够了。

把报错做成可追溯的对象

在 MT5 里用 Print 打一行 "Not enough data." 只能算裸奔。千行级 EA 跑崩时,你根本分不清是哪根线程、哪个函数在哪个服务器时间点的报价缺口触发的。把每条日志封装成 CMessage 对象,才是能事后复盘的做法。 一个消息至少该带四类属性:创建时间、消息来源、消息类型(信息 / 警告 / 报错)、以及可选的系统错误 ID 与交易错误 ID。下面的枚举先定类型,再让类用 Get/Set 和重载构造塞进数据——比如在 OnTick 里发现 CopyClose 不足 100 根就抛警告,连函数名都写进来源字段。 CLog 用单例模式收口:私有静态指针 m_log 只会被 GetLog 建一次,DeleteLog 删一次。两个模块里的指针其实指向同一个对象,避免各写各的日志文件。它还能调 SendNotification 推手机报警,你不在电脑前时这类推送可能帮你少扛一段异常滑点——外汇和贵金属杠杆高,断线漏报的代价倾向放大。 代码里的 CMessage 声明已经把骨架列清:m_type 决定颜色分级,m_source 留调用方名字,m_system_error_id 留给 GetLastError 的返回值。直接把 Message.mqh 和 Logs.mqh 拖进你的 include 目录就能独立用,不用绑原项目。

MQL5 / C++
...
<span class="keyword">class="type">class="kw">double</span> closes[];
<span class="keyword">if</span>(<span class="functions">CopyClose</span>(<span class="functions">Symbol</span>(), <span class="functions">Period</span>(), <span class="number">class="num">0</span>, <span class="number">class="num">100</span>, closes) &lt; <span class="number">class="num">100</span>)
&nbsp;&nbsp; <span class="functions">printf</span>(<span class="class="type">class="kw">string">"Not enough data."</span>);
...
Log.AddMessage(<span class="class="type">class="kw">string">"注意!接收到的柱形数量不足”</span>);
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Logs.mqh |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Copyright class="num">2015, Vasiliy Sokolov. |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2015, Vasiliy Sokolov."</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#include </span>&lt;Object.mqh&gt;
<span class="preprocessor">class="macro">#include </span>&lt;Arrays\ArrayObj.mqh&gt;
<span class="preprocessor">class="macro">#define </span>UNKNOW_SOURCE <span class="class="type">class="kw">string">"unknown"</span>&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 已知消息源</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 消息类型                        </span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">enum</span> ENUM_MESSAGE_TYPE
&nbsp;&nbsp;{
&nbsp;&nbsp; MESSAGE_INFO,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 信息性消息</span>
&nbsp;&nbsp; MESSAGE_WARNING,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 警告性消息</span>
&nbsp;&nbsp; MESSAGE_ERROR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 报错性消息</span>
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|  传递入日志类的消息</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> CMessage : <span class="keyword">class="kw">public</span> CObject
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; ENUM_MESSAGE_TYPE m_type;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 消息类型</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_source;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 消息来源</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_text;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 消息文本</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_system_error_id;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 为系统错误创建一个ID</span>

「用 CMessage 给 EA 留一份可读日志」

在 MT5 里写 EA,最怕的是出事之后不知道现场发生了什么。把交易服务器的返回码、服务器时间和本地时间都塞进一个消息对象,比零散打印变量更容易事后复盘。 下面这段结构定义了 CMessage 的核心字段:m_retcode 存交易服务器返回码,m_server_time 与 m_local_time 分别记录消息创建时的服务器与本地时间,Init 负责按类型、来源和文本初始化。 int m_retcode; // 包含一个交易服务器的返回码 datetime m_server_time; // 消息创建时的交易服务器时间 datetime m_local_time; // 消息创建时的本地时间 void Init(ENUM_MESSAGE_TYPE type,string source,string text); public: CMessage(void); CMessage(ENUM_MESSAGE_TYPE type); CMessage(ENUM_MESSAGE_TYPE type,string source,string text); void Type(ENUM_MESSAGE_TYPE type); ENUM_MESSAGE_TYPE Type(void); void Source(string source); string Source(void); void Text(string text); string Text(void); datetime TimeServer(void); datetime TimeLocal(); void SystemErrorID(int error); int SystemErrorID(); void Retcode(int retcode); int Retcode(void); string ToConsoleType(void); string ToCSVType(void); }; 实际用法看 OnTick:取最近 100 根收盘价,若 CopyClose 不足 100 根,就 new 一个 MESSAGE_WARNING 级别的 CMessage,来源写 __FUNCTION__,文本写 "Not enough data"。 void OnTick(void) { double closes[]; if(CopyClose(Symbol(),Period(),0,100,closes)<100) CMessage message=new CMessage(MESSAGE_WARNING,__FUNCTION__,"Not enough data"); } 外汇与贵金属波动剧烈、杠杆风险高,日志只能帮你还原概率性现象,不能替代风控。开 MT5 把这段结构挂到自己的 include 里,先跑一周看警告都在什么行情节点触发。

MQL5 / C++
class="type">int               m_retcode;              class=class="str">"cmt">// 包含一个交易服务器的返回码
  class="type">class="kw">datetime          m_server_time;          class=class="str">"cmt">// 消息创建时的交易服务器时间
  class="type">class="kw">datetime          m_local_time;           class=class="str">"cmt">// 消息创建时的本地时间
  class="type">void              Init(ENUM_MESSAGE_TYPE type,class="type">class="kw">string source,class="type">class="kw">string text);
class="kw">public:
                     CMessage(class="type">void);
                     CMessage(ENUM_MESSAGE_TYPE type);
                     CMessage(ENUM_MESSAGE_TYPE type,class="type">class="kw">string source,class="type">class="kw">string text);
  class="type">void              Type(ENUM_MESSAGE_TYPE type);
  ENUM_MESSAGE_TYPE Type(class="type">void);
  class="type">void              Source(class="type">class="kw">string source);
  class="type">class="kw">string            Source(class="type">void);
  class="type">void              Text(class="type">class="kw">string text);
  class="type">class="kw">string            Text(class="type">void);
  class="type">class="kw">datetime          TimeServer(class="type">void);
  class="type">class="kw">datetime          TimeLocal();
  class="type">void              SystemErrorID(class="type">int error);
  class="type">int               SystemErrorID();
  class="type">void              Retcode(class="type">int retcode);
  class="type">int               Retcode(class="type">void);
  class="type">class="kw">string            ToConsoleType(class="type">void);
  class="type">class="kw">string            ToCSVType(class="type">void);
};
class="type">void OnTick(class="type">void)
  {
  class="type">class="kw">double closes[];
  if(CopyClose(Symbol(),Period(),class="num">0,class="num">100,closes)<class="num">100)
      CMessage message=new CMessage(MESSAGE_WARNING,__FUNCTION__,"Not enough data");
  }

◍ 用单例类收口消息日志

把日志模块做成单例,是 MT5 EA 里避免多头写文件的常见做法。下面这段 CLog 类把消息对象统一收进一个静态指针 m_log,外部只能靠 GetLog() 拿到同一份实例,构造函数私有化从语法上堵死了重复 new。 类里用 CArrayObj 存 CMessage 指针,并分了两组开关:m_terminal_enable 控制是否打印到终端,m_push_enable 管移动端推送;各自配了优先级枚举,低于阈值的消息就不会骚扰你。默认 DeleteOldLogs 保留 30 天历史,回测跑长周期时磁盘占用可控。 递归标记 m_recursive 用来防 CheckMessage 里二次触发写日志死循环——这种坑实盘跑一周就可能撞上。外汇和贵金属波动大、跳空频繁,日志若漏掉某次推送,复盘时很难定位滑点原因,建议先把 PushPriority 设为 ERROR 级以上再上线观察。 直接把这段类头抄进你的 include 工程,配 Message.mqh 即可编译;开 MT5 用 Total() 看积压条数,能立刻验证单例是否只初始化了一次。

MQL5 / C++
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include "Message.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 实现Singleton消息日志的类
class=class="str">"cmt">//+------------------------------------------------------------------+
class CLog
{
class="kw">private:
   class="kw">static CLog*          m_log;                    class=class="str">"cmt">// 全局静态变量指针
   CArrayObj             m_messages;               class=class="str">"cmt">// 保存消息的列表
   class="type">bool                  m_terminal_enable;       class=class="str">"cmt">//如果你想要打印接受到的消息到交易终端,则设为True
   class="type">bool                  m_push_enable;           class=class="str">"cmt">//如果为True,发送推送通知
   ENUM_MESSAGE_TYPE m_push_priority;             class=class="str">"cmt">// 包含在终端窗口中显示的消息的特定优先级
   ENUM_MESSAGE_TYPE m_terminal_priority;         class=class="str">"cmt">// 包含向移动终端发送推送消息的特定优先级
   class="type">bool                  m_recursive;             class=class="str">"cmt">//标记函数递归调用的标识
   class="type">bool                  SendPush(CMessage* msg);
   class="type">void                  CheckMessage(CMessage* msg);
                        CLog(class="type">void);               class=class="str">"cmt">// 私有构造函数
   class="type">class="kw">string                GetName(class="type">void);
   class="type">void                  DeleteOldLogs(class="type">int day_history = class="num">30);
   class="type">void                  DeleteOldLog(class="type">class="kw">string file_name, class="type">int day_history);
                        ~CLog(class="type">void){;}
class="kw">public:
   class="kw">static CLog*          GetLog(class="type">void);             class=class="str">"cmt">// 获取静态对象的方法
   class="type">bool                  AddMessage(CMessage* msg);
   class="type">void                  Clear(class="type">void);
   class="type">bool                  Save(class="type">class="kw">string path);
   CMessage*             MessageAt(class="type">int index)class="kw">const;
   class="type">int                   Total(class="type">void);
   class="type">void                  TerminalEnable(class="type">bool enable);
   class="type">bool                  TerminalEnable(class="type">void);
   class="type">void                  PushEnable(class="type">bool enable);
   class="type">bool                  PushEnable(class="type">void);
   class="type">void                  PushPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE PushPriority(class="type">void);
   class="type">void                  TerminalPriority(ENUM_MESSAGE_TYPE type);
   ENUM_MESSAGE_TYPE TerminalPriority(class="type">void);

常见问题

先搭一个策略基类把进出场判断收口,再用辅助类管日志和报错,别一上来就堆指标逻辑。
把错误信息封装成带时间、品种、上下文的对象,存进日志类,后续按对象字段过滤排查。
小布可读取EA留下的可读日志与报错对象,自动标红异常并 summarise 成盘后诊断,你只看结论。
单例能保证全EA只有一份日志入口,避免多处写文件错乱,也方便统一格式和级别控制。
可顺带推告警到界面或文件,把策略触发和异常分开存,回测时也能直接导出可读记录。