风险提示:本文为软件开发与技术服务类技术文章,仅用于MQL5程序化开发的技术交流与软件工程实践探讨。文中涉及的停机判据、状态保存方式、持仓对账逻辑与重启检查清单等内容,均为软件工程原理演示与教学用途,不构成任何投资建议、交易策略推荐或收益预期。外汇、贵金属、CFD等杠杆交易存在较高风险,可能导致重大本金损失。文中代码片段仅供学习参考,需经人工审查、编译测试与充分的模拟盘验证后方可使用,直接投入实盘可能造成资金损失。本网站仅提供软件开发技术服务,不涉及任何交易平台运营、经纪业务或投资咨询业务。
EA 出事那一刻,多数人的选择只有两种:打开图表看一眼,停,或者留着等它自己好。这个决定通常几十秒内做完,但它决定后面是修一个问题,还是补一个洞。真正危险的不是 EA 跑错的那一下,是它被重新挂上去的那一下——内存里的记录清零了,账户上的持仓还在,它按空仓的逻辑重算一遍,于是同一方向又开了一笔。这篇讲三件事:什么信号出现必须停、停之前要留下什么、重启后怎么把状态对齐。
一、这5类信号出现,先停EA再谈原因
停的判据必须是可计算的条件。把”感觉不对”翻译成阈值,写进代码,让它自己停——人盯盘会疲劳,代码不会。五类信号里前四类可以完全自动化,第五类看似要靠人判断,其实同样能量化。
滑点连续超限。单次滑点是流动性问题,连续多次是执行环境变了,比”这一单滑得有点多”可靠。连接或行情源中断。TerminalInfoInteger(TERMINAL_CONNECTED) 为 false 是显性的;更隐蔽的是连接显示正常、服务器却不再推报价,这时要看 TimeCurrent() 是否还在前进。净值跌幅越线。用 ACCOUNT_EQUITY 而非 ACCOUNT_BALANCE,浮亏必须算进去。时间戳异常。OnTimer 本该每 N 毫秒触发一次,实际间隔远超设定,说明事件队列被堵住。策略与行情背离。趋势型逻辑在无趋势区间连续止损,止损次数本身就是信号。
string g_stop = ""; // 非空 = 已停机
int g_slip = 0; // 连续滑点超限次数,由成交回调累加
double g_dayBase = 0; // 当日净值基准
datetime g_srvLast = 0; // 上次看到的服务器时间
string StopReason()
{
static int dc = 0;
if(!TerminalInfoInteger(TERMINAL_CONNECTED))
{ if(++dc >= 2) return("connection lost"); } // 连续两次才算断,防瞬时抖动误停
else dc = 0;
datetime t = TimeCurrent();
if(t == g_srvLast && t > 0) return("server time frozen"); // 时间不走 = 行情源停了
g_srvLast = t;
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
if(g_dayBase <= 0) g_dayBase = eq;
if(eq < g_dayBase * (1.0 - InpDayLossLimit)) return("equity drop");
if(g_slip >= InpSlipLimit) return("slippage");
return("");
}
void Halt(const string why)
{
g_stop = why;
SaveState(); // 先落盘,再摘自己
EventKillTimer();
ExpertRemove(); // 触发 OnDeinit(REASON_PROGRAM)
}
风险提示:ExpertRemove() 只把 EA 从图表摘掉,它不碰任何持仓。停机前先确认每个持仓都有服务器端止损单;顺序反了,等于主动卸掉保险。
二、状态恢复的三种模式,别默认挑最省事的那个
恢复的本质是回答一个问题:重启之后的 EA 该相信谁?相信文件里写的、相信终端里真实存在的、还是谁都不信从零开始。三种答案对应三种模式,没有优劣,只有边界。
| 模式 | 恢复依据 | 内存占用 | 恢复耗时 | 适用边界 | 主要风险 |
|---|---|---|---|---|---|
| 冷启动 | 不恢复,OnInit 全部从终端重建 | 最低 | 最短 | 策略逻辑改过、版本号不一致 | 丢失跨周期的信号上下文 |
| 热恢复 | 状态文件 + GlobalVariable 回填 | 中等 | 较长 | 参数未变,仅终端或 VPS 重启 | 过期记录覆盖真实持仓 |
| 部分恢复 | 只回填持仓对账数据,信号缓存丢弃 | 较低 | 中等 | 持仓要对齐,历史信号可重算 | 需多跑一次全量对账 |
这里有个 MQL5 的事实值得单独说,它比 VPS 宕机更常造成孤儿持仓:改图表周期、改品种、改 input 参数、重新编译,都会走一遍 OnDeinit → OnInit,原因码分别是 REASON_CHARTCHANGE(3)、REASON_PARAMETERS(5)、REASON_RECOMPILE(2)。也就是说,”我只是把周期从 M5 调到 M15″在 EA 看来就是重启了一次——静态变量和全局变量全部重新初始化,内存里的持仓计数归零,而账户上的持仓一笔没少。
知识点:全局变量与 static 变量只在两次 OnTick 之间保值,跨过一次重新初始化就回到初值。持仓状态不能只活在内存里。
重点提示:终端全局变量做持久化有三条边界:只能存 double、满 4 周未访问会自动删除、不调 GlobalVariablesFlush 则落盘时机不可控。它适合存几个数字,不适合存一份状态;键名要带品种与 magic 隔离。
// 只存"最小可重建集",其余全部从终端读
void SaveState()
{
string k = "EA_" + _Symbol + "_" + IntegerToString(InpMagic);
GlobalVariableSet(k + "_ver", (double)EA_VERSION); // GV 只能存 double
GlobalVariableSet(k + "_vol", (double)g_trackVol);
GlobalVariableSet(k + "_dir", (double)g_trackDir);
GlobalVariablesFlush(); // 主动落盘,别等终端关
int h = FileOpen(k + ".state", FILE_WRITE|FILE_TXT|FILE_ANSI);
if(h == INVALID_HANDLE) return; // 文件兜底:能存字符串与时间
FileWrite(h, IntegerToString(EA_VERSION));
FileWrite(h, DoubleToString(g_trackVol, 2));
FileWrite(h, IntegerToString(g_trackDir));
FileWrite(h, TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS));
FileClose(h);
}
操作参考:逻辑改过就冷启动,参数未变只是进程重启就热恢复,只有持仓对不上就部分恢复。判断”改过”靠写进文件的 EA_VERSION 比对,不靠记忆。
三、持仓对不上:三步诊断,别急着平仓重开
对账的原则只有一条:终端才是真相源。PositionsTotal 与 PositionSelect 读出来的是券商认的,EA 内存里的记录只是缓存。缓存错了就改缓存,不要为了让缓存”看起来对”去动真实持仓——后者是把一个数据问题升级成一次真实交易成本。
// 以终端为准重建净头寸,只认本 EA 的 magic
double RebuildNet(const long magic)
{
double buy = 0, sell = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong tk = PositionGetTicket(i); // 该函数会同时选中这笔持仓
if(tk == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; // 别的品种不碰
if(PositionGetInteger(POSITION_MAGIC) != magic) continue; // 别人的仓位不碰
double v = PositionGetDouble(POSITION_VOLUME);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) buy += v;
else sell += v;
}
return(buy - sell); // 净头寸:正为多,负为空
}
第一步,枚举。按品种加 magic 双重过滤算净头寸。手动开的仓、别的 EA 开的仓都不该进这个数,混进来会让后面每一步都错。第二步,分类差异。gap 等于真实值减记录值。为正说明终端多出一块,可能是部分成交补量或人工加仓;为负说明记录里有幽灵仓位,被手动平掉或被服务器端止损打掉。要分清是哪一种,用 HistorySelect 拉这段时间的成交记录看 magic 与 entry 类型。第三步,处置。孤儿仓位方向与当前信号一致就接管跟踪,冲突才平掉;幽灵记录直接清掉,不要为了”对上”补开一笔。若 gap 小于该品种最小手数,那是浮点误差,忽略。
知识点:10009 DONE 表示已完成但 volume 可能小于请求手数;10010 DONE_PARTIAL 明确只完成一部分;10008 PLACED 是挂单已被接受、尚未成交。只看 retcode 就判断”开成功了”会误判。
风险提示:平仓重开不是万能修复。一平一开之间要付点差滑点,还可能撞上拒单、报价过期或流动性空档,亚洲早盘与周五收盘前尤其容易开不回来。只有方向冲突或来路完全不明时才平。
四、重启前的8项检查
这8项按”会不会直接导致下错单”排序,越靠前越致命。逐项过一遍花不了几秒,却能挡掉多数重启事故。每项都要写成可判定的条件,不写成要靠回忆的描述。
| # | 检查项 | 通过判据 | 不通过怎么办 |
|---|---|---|---|
| 1 | 持仓归属 | 按品种加 magic 枚举出的手数与记录一致 | 进对账流程,不动仓位 |
| 2 | 挂单清理 | 无过期、无失效、无重复挂单 | 撤单后按当前逻辑重挂 |
| 3 | 状态文件可读 | 存在、字段完整、时间戳不早于上次运行 | 放弃热恢复,改冷启动 |
| 4 | 时间基准 | TimeCurrent 与 TimeLocal 偏差在预期区间 | 不交易,等时间同步恢复 |
| 5 | 交易权限 | ACCOUNT_TRADE_EXPERT、TERMINAL_TRADE_ALLOWED、MQL_TRADE_ALLOWED 均为 true | 进只读模式,不报单只告警 |
| 6 | 内存残留 | 指标句柄与图表对象已在上次 OnDeinit 释放 | 记录后重启终端,防句柄泄漏 |
| 7 | 日志连续性 | 上一份日志里没有未闭合的请求 ID | 人工核对最后几笔订单再启动 |
| 8 | 保证金余量 | OrderCalcMargin 估算的最坏开仓成本低于可用保证金 | 降手数或不开新仓 |
重点提示:8 项要在 OnInit 里跑完再决定是否交易,不能”先跑起来再说”。任一项不通过就进只读模式——不报单只告警,比带着不确定状态开跑安全。第 7 项的做法在上一篇日志系统文章里展开过,这里只当前置条件引用。
五、VPS重启后的四步恢复
有个坑不少人踩过:周末重启服务器,EA 挂上去,看图表觉得一切正常,实际上它一次 OnTick 都没执行过。MQL5 的 OnTick 由价格变动触发,周末没有报价,第一个 tick 要等到开盘才来。所以恢复逻辑写在 OnTick 里等于没写——它得放在 OnInit 里做一次性检查,或者放在 OnTimer 里,定时器与行情无关。
重点提示:挂上去没报错不等于跑起来了。判断依据是 OnTimer 有没有在动,不是图表上的笑脸图标。省事的做法是 OnInit 末尾写一行 init done,第一个 OnTick 再写一行 first tick,两行都出现才算接上行情。
① 故障发生。先分清是终端进程挂了、VPS 挂了、还是只是网络断了。看终端日志最后一条的时间戳,跟系统时间对比,差多少就是断了多久。② 判断影响面。故障期间有没有持仓、有没有被止损触发。用 HistorySelect 拉这段时间的成交记录,看有没有 DEAL_ENTRY_OUT。③ 选择恢复模式。先看代码版本号有没有变,再看持仓是否对得上。④ 验证一致性。OnInit 里先跑一遍对账,结果写进日志第一行,对账不平就不开单。
操作参考:四步顺序不能颠倒。故障期间被止损过,记录里还留着已平掉的仓位,必须走对账修正;什么都没发生,热恢复就够。顺序反了,就会拿过期记录覆盖真实持仓。
进阶思考:更省事的解法是把”状态”从 EA 里删掉:持仓、挂单、当日盈亏全部现算,内存只留本 tick 的中间变量。重启代价接近零,因为它没有需要恢复的东西。能重建的数据,才不怕丢。
停机、重启、对账,这三件事的顺序是固定的:先保住仓位,再保住记录,最后才是保住策略。反过来做——先想策略怎么继续跑,再想记录怎么接上,最后才想起仓位——那就不是应急处理,而是给下一次事故铺路。
风险提示:本文内容仅供软件开发与技术学习参考,不构成任何投资建议或交易指导。程序化交易存在较高风险,可能导致重大本金损失。文中涉及的停机判据、状态保存、持仓对账与检查清单代码均为技术原理演示,未经完整工程化封装,直接复制使用可能存在边界条件遗漏、文件句柄泄漏、多图表并发写入冲突、magic 号重复导致误判等缺陷,需经人工审查、编译测试与充分的模拟盘验证。文中出现的返回码数值(10008、10009、10010 等)与原因码(REASON_CHARTCHANGE 等)依据 MQL5 官方文档整理,具体含义与行为以对应版本官方文档及实测结果为准。实盘应用前请自行决策并承担相应风险。本网站仅提供软件开发技术服务,不涉及任何交易平台运营、经纪业务或投资咨询业务。
第三方免责声明:本文仅代表作者个人技术观点,与任何经纪商、交易平台或金融机构无关。文中提及的 MetaTrader 5、MQL5、MetaEditor、ExpertRemove、OnInit、OnDeinit、OnTick、OnTimer、EventKillTimer、GlobalVariableSet、GlobalVariablesFlush、GlobalVariableGet、PositionsTotal、PositionGetTicket、PositionGetInteger、PositionGetString、PositionGetDouble、OrderCalcMargin、HistorySelect、TerminalInfoInteger、AccountInfoDouble、FileOpen、FileWrite、FileClose、TRADE_RETCODE_* 系列常量、REASON_* 系列常量等名称与产品,其商标与权利归各自所有者所有,本文仅作技术说明与交流引用,不构成任何推荐、背书或商业合作关系。文中关于事件生命周期、全局变量持久化行为、部分成交返回码与券商执行差异的描述基于公开资料与工程实践,具体表现以各平台官方说明与实测为准。读者应自行核实相关信息,因使用本文内容而产生的直接或间接损失,作者不承担任何责任。本文不推荐任何具体的经纪商、交易平台或金融产品。
扫码关注,获取更多EA开发干货
微信公众号
视频号
