1,000,000$ 交易计划
以量化交易为生

在MT5里,EA崩溃是致命的——它会在日志里留下一行日志,然后EA被强制从图表卸载

下面每个案例都对应有一个测试 EA, 通过真实日志,你能看到 EA 在哪一行、具体什么原因崩溃。

安全提示:以下代码是教学代码,请勿将错误用法应用到真实场景中。

01最常见的问题:数组越界

array out of range,访问不存在的下标。

C01循环多走一步(off-by-one)array out of range

遍历持仓单时,循环条件写成了 <=。

❌ 错误写法
// 缓存了 5 个单号,合法下标 0..4
int n = ArraySize(tickets);          // n = 5
for(int i = 0; i <= n; i++)           // <= 让最后一轮访问 tickets[5]   PrintFormat("检查持仓 i=%d ticket=%I64u", i, tickets[i]);
智能交易系统 — SS_C01_OffByOne
11:50:51[C01] 缓存了 5 个持仓单号,合法下标 0..4
11:50:51[C01] 检查持仓 i=3 ticket=100004
11:50:51[C01] 检查持仓 i=4 ticket=100005
11:50:51array out of range in 'SS_C01_OffByOne.mq5' (67,65)

为什么下标走到 i = n = 5 时,tickets[5] 已经超出了 0..4。循环打印到 i=4 一切正常,第 6 轮直接崩。

✅ 正确写法
for(int i = 0; i < n; i++)             // 下标只到 n-1   PrintFormat("检查持仓 i=%d ticket=%I64u", i, tickets[i]);
C02在空数组里取元素array out of range

取数组的最后一个元素,但是数组为空。

❌ 错误写法
ArrayResize(g_deals, 0);             // 还没有任何成交
int n = ArraySize(g_deals);          // n = 0
DealInfo last = g_deals[n - 1];      // n=0 时访问 g_deals[-1]
智能交易系统 — SS_C02_EmptyArrayLast
11:51:29[C02] 当前喊单成交记录数=0
11:51:29array out of range in 'SS_C02_EmptyArrayLast.mq5' (69,33)

为什么ArraySize()-1 在空数组上等于 -1,这是一个非法下标。

✅ 正确写法
int n = ArraySize(g_deals);          // n = 0
if(n > 0) {    // 确认非空后再取
    DealInfo last = g_deals[n - 1];
}
一句话:从数组取元素之前,先判断数组是否为空。
C06二维数组用 ArraySize 当行数array out of range

错误地使用了 ArraySize 作为下标取二维数组。

❌ 错误写法
double g_table[4][3];                 // 4 行 × 3 列
int rows = ArraySize(g_table);       // 拿到的是元素总数 4×3 = 12for(int r = 0; r < rows; r++)
   PrintFormat("第 %d 行 ...", r, g_table[r][0]);
智能交易系统 — SS_C06_ArraySize2D
11:53:53[C06] ArraySize=12,ArrayRange(第1维)=4
11:53:53[C06] 第 3 行 ticket=1004 lots=0.04 price=1.08530
11:53:53array out of range in 'SS_C06_ArraySize2D.mq5' (71,60)

为什么ArraySize 返回的是全部元素个数,取真正的行数要用 ArrayRange(arr, 0)。打印到第 3 行后,r=4 就越界了。

✅ 正确写法
int rows = ArrayRange(g_table, 0);   // 第 1 维长度 = 4for(int r = 0; r < rows; r++) ...
一句话:需要多维数组的行数时使用 ArrayRange(arr, 维序号)。

02遍历时修改数组

在遍历数组(比如持仓)时,修改数组的长度会直接导致下标越界,有时候EA崩掉反而是件好事,如果这个问题没有及时暴露出来,等到特定条件触发这个bug时,就很难弥补了。

C03遍历时裁剪数组(缓存了长度)array out of range

遍历某个列表时,在特定条件下调用 ArrayRemove 裁剪数组,导致数组下标越界。

❌ 错误写法
int n = ArraySize(g_tickets);        // 长度只取一次(=6)for(int i = 0; i < n; i++)            // n 始终是 6
   if(IsClosed(g_tickets[i]))
      ArrayRemove(g_tickets, i, 1);   // 删一个,数组就短一截
智能交易系统 — SS_C03_RemoveWhileIterating
11:52:02[C03] i=0 检查 ticket=1001(当前长度=6)
11:52:02[C03] 已平仓,移出列表 → [1001,1003,1004,1005,1006]
11:52:02[C03] i=3 检查 ticket=1005(当前长度=5)
11:52:02array out of range in 'SS_C03_RemoveWhileIterating.mq5' (88,74)

为什么在遍历数组时删除数组元素,而数组长度在一开始时用变量存起来了,没有变化。

✅ 正确写法:倒序遍历
for(int i = ArraySize(g_tickets) - 1; i >= 0; i--)   if(IsClosed(g_tickets[i]))
      ArrayRemove(g_tickets, i, 1);   // 删除只影响已经检查过的位置
一句话:要在遍历中删元素,应该从后往前删。
C04循环里调的函数修改了数组array out of range

这种情况加大了调试问题的难度,单看遍历代码很难看出问题,但是执行到循环里调用的函数时,如果在函数内部删除了数组元素,就有可能触发数组访问越界。

❌ 错误写法
int n = ArraySize(g_follow);         // 缓存长度 = 6
for(int i = 0; i < n; i++)
   HandleOrder(g_follow[i]);          // 内部可能触发 CleanupClosed(),把数组删到 3
智能交易系统 — SS_C04_HiddenShrinkInCallee
11:52:26[C04] i=1 处理 ticket=1002(当前长度=6)
11:52:26[C04] [同步模块] 清理已平仓单,列表长度变为 3
11:52:26[C04] i=2 处理 ticket=1006(当前长度=3)
11:52:26array out of range in 'SS_C04_HiddenShrinkInCallee.mq5' (102,70)

为什么改动数组的代码藏在被调用的函数里,在外层循环时根本看不出来,这是最难查的一种越界。

✅ 正确写法:循环里只标记,循环后统一清理
for(int i = 0; i < n; i++) HandleOrder(g_follow[i]); // 只打标记 g_needCleanup
if(g_needCleanup) CleanupClosed();   // 遍历彻底结束后,再动数组
一句话:正在遍历的数组,别让任何函数去修改它,如果一定要改,就推迟到循环结束以后。

03除零与指针越界

这两类问题只需要一行就能让EA直接崩溃。

C07除数为 0zero divide

在除法中使用变量,而变量有可能为0。

❌ 错误写法
int perMaster = riskSlots / InpMasterCount;   // 账户数 = 0 → 整数除零
智能交易系统 — SS_C07_ZeroDivide
11:54:15zero divide, check divider to avoid this error in 'SS_C07_ZeroDivide.mq5' (68,33)

为什么整数除以 0 直接崩溃。而浮点除以 0 不崩,但会得到 inf / nan。

✅ 正确写法:除法前先卡住除数
if(InpMasterCount <= 0) { Print("账户数为 0,不分配额度"); }else perMaster = riskSlots / InpMasterCount;
一句话:任何除法前都先判断除数。
C08NULL 指针 / 悬空指针 / 重复 deleteinvalid pointer access

对象已为空,访问它就会崩溃。

❌ 错误写法
COrder *o = FindOrder(9999);         // 找不到,返回 NULL
PrintFormat("手数=%.2f", o.volume);    // 对 NULL 取成员
智能交易系统 — SS_C08_InvalidPointer
11:54:34[C08] 查找 ticket=9999 的结果:POINTER_INVALID
11:54:34invalid pointer access in 'SS_C08_InvalidPointer.mq5' (94,36)

为什么对象 delete 后为悬空指针,无法再被访问。

✅ 正确写法:先查指针,delete 后立刻置 NULL
if(CheckPointer(o) == POINTER_INVALID) { /* 当新单处理 */ return; }delete g_list[0]; g_list[0] = NULL; g_lastHandled = NULL; // 所有引用一起清
一句话:用指针前先 CheckPointer();delete 之后,把指向同一对象的每一个引用都置 NULL。

04循环失控

下面演示如何把栈撑爆,或者如何把EA冻结。

C09用递归实现的失控循环stack overflow

服务器持续拒单(比如挂单价离市价太近),重试函数失败后调用自己,没有次数上限。

❌ 错误写法
bool SendWithRetryBug(const int depth) {
   if(FakeOrderSend(req, res)) return(true);
   return SendWithRetryBug(depth + 1);   // 失败就调用自己,没有出口}
智能交易系统 — SS_C09_StackOverflow(复刻输出)
—[C09] 第 1000 次被拒(retcode=10015),继续递归重试…
—[C09] 第 2000 次被拒(retcode=10015),继续递归重试…
—stack overflow in 'SS_C09_StackOverflow.mq5'

为什么每递归一层就占用一截调用栈,永不返回,栈很快就会被撑爆。结果是MT5直接崩溃。

✅ 正确写法:有限次数的循环重试
for(int attempt = 1; attempt <= InpMaxRetries; attempt++) {   if(FakeOrderSend(req, res)) return(true);
   Sleep(200);
}
Print("达到重试上限,放弃本单并发出通知"); // 交给上层处理
一句话:重试用带上限的 for 循环 + Sleep,而不是递归。
C11无限循环等待服务器返回不崩溃,整体冻结

发出开仓请求后,用 while 循环死等持仓出现;服务器如果没有返回,则EA会一直卡死。

❌ 错误写法
while(!PositionReady(g_ticket))        // 没有超时,也不检查 IsStopped()   Sleep(100);                         // 回报不到就一直卡在这里
智能交易系统 — SS_C11_InfiniteWait
12:04:52[C11] 已发出开仓请求,等待持仓出现…
12:05:02[C11] 等到了持仓,但 EA 已卡住 10063 ms,期间所有事件都没处理
12:05:02[C11] 心跳 #1:距上次定时器 10063 ms(正常应约 1000 ms)

为什么如果服务器无响应,EA就会一直卡死,只能手动移除EA,卡死导致的问题比直接崩溃更严重。

✅ 正确写法:短超时 + 定时器轮询
while(!IsStopped() && GetTickCount() - start < InpTimeoutMs) {   if(PositionReady(g_ticket)) break;
   Sleep(100);
}
if(!ready) { g_pending = true; /* 标记「待核对」,交给 OnTimer 继续轮询 */ }
一句话:代码里尽量不要 while(true) 无限循环,如果一定要阻塞等待,要添加超时机制。

本文代码仅用于教学演示,请在模拟账户环境下运行测试。本文内容不构成任何投资建议。

发表回复 取消回复