构建自动运行的 EA(第 11 部分):自动化(III)(基础篇)
「用定时器让EA脱离图表事件自跑」
MT5 的 EA 默认靠 OnTick、OnTimer 等事件驱动,但很多自动化逻辑需要按固定节奏轮询,而不是等行情 tick。用 EventSetTimer 挂一个毫秒级定时器,就能让 EA 在后台按设定间隔执行检查,不依赖当前图表是否活跃。 下面这段是最小可用骨架:先到 OnInit 里开定时器,OnDeinit 里关掉,避免残留。OnTimer 就是你的轮询入口,放订单状态检查、持仓再平衡都行。 [CODE] int OnInit() { EventSetTimer(60); // 每 60 秒触发一次 OnTimer return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { EventKillTimer(); // 注销定时器,防止 EA 卸载后还占资源 } void OnTimer() { // 这里写你的自动化检查逻辑 // 例如:遍历持仓、检查止损距离、触发部分平仓 } [/CODE] 逐行看:EventSetTimer(60) 的参数是秒,不是毫秒,写错单位会让轮询快到拖垮终端;EventKillTimer 必须配对出现,否则 MT5 日志里会留「timer already set」类警告。外汇与贵金属杠杆高,用定时器批量操作前先在策略测试器跑 2023 年 5 月那波利率周,观察滑点是否吃掉你的间隔容差。 实盘里把间隔从 60 秒调到 30 秒,订单状态同步会更快,但 CPU 占用约翻倍——老笔记本跑多个 EA 时得权衡。
class="type">int OnInit() { EventSetTimer(class="num">60); class=class="str">"cmt">// 每 class="num">60 秒触发一次 OnTimer class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { EventKillTimer(); class=class="str">"cmt">// 注销定时器,防止 EA 卸载后还占资源 } class="type">void OnTimer() { class=class="str">"cmt">// 这里写你的自动化检查逻辑 class=class="str">"cmt">// 例如:遍历持仓、检查止损距离、触发部分平仓 }
把 EA 代码放进三个文件夹再谈自动化
走到 100% 自动化前,最后一步不是改旧代码,而是删掉交易者和 EA 之间的交互点,换成自动触发器。如果实现自动化时还得动原有逻辑,说明规划本身有缺陷,得回炉重做。 一套能长期跑的 EA 至少要满足几条:健壮(无破坏完整性的主错误)、可靠(扛得住多种险情仍无故障)、稳定(不能时好时坏把平台搞崩)、可扩展(增长不靠大量重写)、封装(只对外部暴露真正要用的功能)、迅捷(代码拖慢再好的模型也白搭)。这些多半要靠面向对象模型,本系列从开头就全程用类,不是偶然。 实操上,无论程序多简单,都该习惯分三个文件夹维护代码。开发文件夹里做所有新建和修改;测完移入工作文件夹,这里若只修小缺陷可就地改,要大改就退回开发;反复跑无新错再移入稳定文件夹,此处只进不出,绝不加新代码。坚持一阵,你会攒出一套能快速安全复用的函数库——在外汇和贵金属这种实时、最坏场景频发的市场里,没人敢用扛不住风险的代码。 手动系统的本质看交互链就懂:交易者不直接碰 EA,而是通过平台发事件给 EA,EA 向平台发请求、平台转交易服务器、响应原路返回。箭头不越顶点即手动标志。EA 出故障平台没事,新手却常怪平台。没参与过平台开发就别试图改它的工作方式,让代码去适配平台需求。 先手动跑一段再自动化,目的就是拿真实交互验证代码,精准造出所需功能。上一篇的代码进稳定文件夹,本文新增的进开发文件夹,出问题能退两个版本,编码速度反而更快,外汇贵金属高风险下这种冗余不是浪费。
◍ 把本地时钟换成服务器时间
EA 计时最容易被忽略的坑,是用了电脑本地时间而不是经纪商服务器时间。原代码里先用 TimeLocal 取本地时间,再转成结构,等价于假设你的电脑时钟和服务器同步;但大多数人没挂 NTP,本地时间偏几分钟是常事,EA 就可能提前进场或延后离场。 下面这段把取时间的方式换掉了:删掉 TimeToStruct(TimeLocal(), mdt),改成直接 TimeCurrent(mdt)。TimeCurrent 返回的是终端连接的交易服务器时间,和报价面板里看到的一致,不再受本机时钟漂移影响。 virtual const bool CtrlTimeIsPassed(void) final { datetime dt; MqlDateTime mdt; TimeToStruct(TimeLocal(), mdt); // 旧:取本机时间再转结构 TimeCurrent(mdt); // 新:直接取服务器时间填入结构 dt = (mdt.hour * 3600) + (mdt.min * 60); return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt)); } 改动不大,但稳定性提升明显:交易者不会因为 EA 比预期早几秒下单就怀疑平台出 bug。外汇和贵金属波动受数据跳空影响,本地时间与服务器时间错位会放大滑点风险,换用 TimeCurrent 是低成本修正。 订单发送那段也顺手改了错误处理。原逻辑一遇错就弹消息框,全自动 EA 卡在等人工点确认上就废了;现在改成先 ResetLastError,再 OrderCheck,只有 _LastError 为 ERR_SUCCESS 才 OrderSend,异常只在终端打印,不阻断自动循环。
class="kw">virtual const class="type">bool CtrlTimeIsPassed(class="type">void) final { class="type">class="kw">datetime dt; class="type">MqlDateTime mdt; TimeToStruct(TimeLocal(), mdt); TimeCurrent(mdt); dt = (mdt.hour * class="num">3600) + (mdt.min * class="num">60); class="kw">return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt)); } class="type">ulong ToServer(class="type">void) { MqlTradeCheckResult TradeCheck; class="type">MqlTradeResult TradeResult; class="type">bool bTmp; ResetLastError(); ZeroMemory(TradeCheck); ZeroMemory(TradeResult); bTmp = OrderCheck(m_TradeRequest, TradeCheck); if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult);
「下单失败时的错误捕获与返回」
在 MT5 的订单封装函数里,错误码检查必须紧跟交易请求之后。上面这段逻辑用 _LastError 判断上一次交易函数调用的系统状态,若不等于 ERR_SUCCESS 说明下单链路在某个环节断了。 两种提示方式可以并存:弹窗 MessageBox 适合人工盯盘时立刻发现异常,而 PrintFormat 写终端日志更适合自动化策略的后台排查。注意前者用了 GetLastError() 重新取码,后者直接读 _LastError,二者在连续调用时可能拿到不同值,这是个容易踩的坑。 最后一行把结果往外抛——成功就返回订单 ticket,失败返回 0。实盘里用返回 0 作哨兵比用布尔更直观,调用方拿不到有效 order ticket 就该停止后续加仓动作。外汇与贵金属杠杆高,这类静默失败若不被捕获,可能让仓位管理逻辑误以为成交了。
if (_LastError != ERR_SUCCESS) MessageBox(StringFormat("Error Number: %d", GetLastError()), "Order System", MB_OK); if (_LastError != ERR_SUCCESS) PrintFormat("Order System - Error Number: %d", _LastError); class="kw">return (_LastError == ERR_SUCCESS ? TradeResult.order : class="num">0);
把宏定义换成类内常量,给自动化留接口
要把半自动逻辑改成 100% 自动化 EA,第一步是干掉两个暴露给交易者的宏定义:def_MAX_LEVERAGE 和 def_ORDER_FINISH。原文里它们被直接删掉,换成类内私有变量 MaxLeverage 与 IsOrderFinish,由程序员在构造函数里写死,交易者改不了。 用变量替宏会有极小的性能代价——访问常量比访问变量快,哪怕只差几个机器周期。但换来的是类重用性,下一篇接自动化时你会明显感觉到移植成本低了一截。作为程序员,除了构造函数初始化那一下,别在任何地方改这两个值,否则逻辑会歪。 构造函数因此多了两个入参:const bool IsOrderFinish 和 const uint MaxLeverage。实例化 C_Manager 的所有地方都要同步改调用,原文把所有移除段都高亮替换了,可用性也顺手提了一级。 光改定义不够,自动化系统要更自由地碰订单发送,所以在 C_Manager 里加了个 ClosePosition 类的封装函数。加完编译器报了警告(图例 03 那种),这类警告不能无视——编译器看不懂你的意图时,生成的代码就谈不上 100% 可靠。个人不建议关掉平台警告,留着默认设置反而最稳。 解决警告有两条路:要么把对 C_Orders::ClosePosition 的调用改成新函数,要么显式告诉编译器引用 C_Orders 类。原文选了前者,在析构函数里塞进平仓调用;但若挂单已转持仓且没被 C_Manager 捕获,就保留高亮的那行 C_Orders 引用。另一处修改把原来简单清挂单号的逻辑换成「挂单删不掉就删对应持仓」的复杂版,正是自动化必需的。 这些零碎改动攒起来,系统级的代码重用和测试面都扩了。外汇和贵金属杠杆高、滑点狠,跑之前务必在 MT5 策略测试器里先验证构造函数参数传递没错位。
class="macro">#include "C_ControlOfTime.mqh" class="macro">#define def_MAX_LEVERAGE class="num">10 class="macro">#define def_ORDER_FINISH false class C_Manager : class="kw">public C_ControlOfTime { class="kw">private : class="kw">struct st00 { class="type">class="kw">double FinanceStop, FinanceTake; class="type">uint Leverage, MaxLeverage; class="type">bool IsDayTrade, IsOrderFinish; }m_InfosManager; C_Manager(const class="type">ulong magic, class="type">class="kw">double FinanceStop, class="type">class="kw">double FinanceTake, class="type">uint Leverage, class="type">bool IsDayTrade, class="type">class="kw">double Trigger, const class="type">bool IsOrderFinish, const class="type">uint MaxLeverage) :C_ControlOfTime(magic), m_bAccountHedging(false),
◍ 挂单触发后把持仓属性抓进结构
这段初始化逻辑跑在挂单成交之后,负责把风控参数和账户杠杆上限先塞进 m_InfosManager,再交给后续的位置监控函数使用。
构造函数里先 ResetLastError 清掉上一次的错误码,ZeroMemory 把 m_Position 整个置零,避免残留旧持仓数据导致误判。随后把外部传入的 IsOrderFinish、MaxLeverage、FinanceStop、FinanceTake、Leverage、IsDayTrade 逐一写入管理结构。
SetInfoPositions 是真正读持仓的地方:杠杆按 持仓量 / VolMinimal 折算成整数倍,例如 VolMinimal=0.01 时 0.10 手对应杠杆值 10。方向、开仓价、SL、TP 全部用 PositionGetDouble / PositionGetInteger 实时拉取,写进 m_Position 供小布盯盘做动态盈亏测算。
开 MT5 把这段贴进 EA 的挂单回调节点,改 VolMinimal 对照自己券商最小交易量,就能验证杠杆折算是否和你后台显示一致。外汇与贵金属波动剧烈,高杠杆可能放大亏损。
m_TicketPending(class="num">0), m_Trigger(Trigger) { class="type">class="kw">string szInfo; ResetLastError(); ZeroMemory(m_Position); m_InfosManager.IsOrderFinish = IsOrderFinish; m_InfosManager.MaxLeverage = MaxLeverage; m_InfosManager.FinanceStop = FinanceStop; m_InfosManager.FinanceTake = FinanceTake; m_InfosManager.Leverage = Leverage; m_InfosManager.IsDayTrade = IsDayTrade; class="kw">inline class="type">int SetInfoPositions(class="type">void) { class="type">class="kw">double v1, v2; class="type">int tmp = m_Position.Leverage; m_Position.Leverage = (class="type">int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal); m_Position.IsBuy = ((class="type">ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY; m_Position.TP = PositionGetDouble(POSITION_TP); v1 = m_Position.SL = PositionGetDouble(POSITION_SL); v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);