通用EA:自定义策略和辅助交易类(第三章)(基础篇)
通用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 目录就能独立用,不用绑原项目。
... <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) < <span class="number">class="num">100</span>) <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">//| Logs.mqh |</span> <span class="comment">class=class="str">"cmt">//| Copyright class="num">2015, Vasiliy Sokolov. |</span> <span class="comment">class=class="str">"cmt">//| [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> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#include </span><Object.mqh> <span class="preprocessor">class="macro">#include </span><Arrays\ArrayObj.mqh> <span class="preprocessor">class="macro">#define </span>UNKNOW_SOURCE <span class="class="type">class="kw">string">"unknown"</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="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">enum</span> ENUM_MESSAGE_TYPE { MESSAGE_INFO, <span class="comment">class=class="str">"cmt">// 信息性消息</span> MESSAGE_WARNING, <span class="comment">class=class="str">"cmt">// 警告性消息</span> MESSAGE_ERROR <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">class</span> CMessage : <span class="keyword">class="kw">public</span> CObject { <span class="keyword">class="kw">private</span>: ENUM_MESSAGE_TYPE m_type; <span class="comment">class=class="str">"cmt">// 消息类型</span> <span class="keyword">class="type">class="kw">string</span> m_source; <span class="comment">class=class="str">"cmt">// 消息来源</span> <span class="keyword">class="type">class="kw">string</span> m_text; <span class="comment">class=class="str">"cmt">// 消息文本</span> <span class="keyword">class="type">int</span> m_system_error_id; <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 里,先跑一周看警告都在什么行情节点触发。
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() 看积压条数,能立刻验证单例是否只初始化了一次。
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);