我给你讲个真事。一个"回测完美"的网格EA,上线实盘第一小时就爆仓了。
回测数据漂亮得不行:盈利因子2.3,夏普比率1.8,最大回撤12%。模拟盘跑了俩礼拜,稳如老狗。
实盘上线60分钟内,它在券商那边开了三张根本不存在的"幽灵单",然后试图用
OrderClose()去平这些单,静默失败后,基于已经错乱的内部状态又开了新的仓位。问题不在EA的策略逻辑。问题出在"管道"上——那些回测环境永远不会告诉你的细节。回测太"仁慈"了。
14年EA救火经验告诉我,这个模式重复出现。下面这五个结构性缺陷,每一个都真实地爆过实盘账户。
缺陷一:把OrderSend()当"稳了"
最常见的错误,就是把
OrderSend()当成必成操作。``
mql4
// ❌ 问题:不检查返回值,不处理错误
OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp);
gridLevel++; // 不管订单是否成交,都自增!
`
在策略测试器里,每个订单都能瞬间成交。实盘呢?重报价、保证金不足、交易上下文繁忙——都是静默失败。EA的内部状态和券商的实际状态就这么分道扬镳了。
修复不只是检查返回值——还要验证订单真的存在:
`mql4
// ✅ 修复:检查返回值、验证存在、再更新状态
int ticket = OrderSend(Symbol(), OP_BUY, 0.10, Ask, 3, sl, tp);
if(ticket < 0) {
Print(StringFormat("%s: OrderSend失败 | 错误码=%d", __FUNCTION__, GetLastError()));
return; // 不更新状态
}
// 验证订单是否真的被挂上了
if(!OrderSelect(ticket, SELECT_BY_TICKET)) {
Print("订单#", ticket, "不存在,尽管OrderSend返回了ticket!");
return;
}
gridLevel++; // 确认成交后才更新
`
参考来源: MQL4官方文档明确指出,OrderSend()在交易请求被拒时返回-1。GetLastError()必须在调用后立即使用。但至今我仍能看到代码中缺少这个基本检查。
缺陷二:Error 130——真正的隐形杀手
Error 130("无效止损")是MQL4中最被误解的错误。它通常不是你想象的那个意思。
我修过一个移动止损EA,在每次报价时OrderModify()都被拒绝,报错130。EA日志里刷着"移动止损已激活",但实际止损位几小时都没动过。
多数开发者忽略的关键:Error 130不只是说你设的止损价"不对"。它说你设的价格违反了券商的MODE_STOPLEVEL约束——而这个约束会随市场条件动态变化。
`mql4
// ❌ 问题:硬编码止损距离
double newSL = Bid - 20 Point;
OrderModify(ticket, price, newSL, tp, 0);
// ✅ 修复:查询MODE_STOPLEVEL并做校验
int stopLevel = (int)MarketInfo(Symbol(), MODE_STOPLEVEL);
double minStopDistance = stopLevel Point;
double maxStopDistance = 1000 Point; // 券商特定上限
double newSL = Bid - 20 Point;
// 校验最小距离
if(Bid - newSL < minStopDistance) {
newSL = Bid - minStopDistance;
Print("止损已调整至最小允许值: ", newSL);
}
// 校验最大距离(很多券商有限制)
if(Bid - newSL > maxStopDistance) {
newSL = Bid - maxStopDistance;
Print("止损已调整至最大允许值: ", newSL);
}
// 还要检查MODE_FREEZELEVEL(限价单专用)
int freezeLevel = (int)MarketInfo(Symbol(), MODE_FREEZELEVEL);
if(freezeLevel > 0 && MathAbs(newSL - Ask) < freezeLevel * Point) {
Print("价格离冻结区太近,等待。");
return;
}
if(OrderModify(ticket, price, newSL, tp, 0)) {
Print("止损修改成功。生效止损: ", newSL);
} else {
Print(StringFormat("OrderModify失败 | 错误码=%d | 止损=%.5f",
GetLastError(), newSL));
}
`
独家观点: 多数开发者把MODE_STOPLEVEL当成静态值,只在OnInit()里查一次。这是错的。有些券商在大波动或新闻时段会动态调整停损距离。我遇到过一家券商,平时MODE_STOPLEVEL是0,但重大新闻发布时直接拉到50。那个每次报价都动态查询的EA活下来了;那个在启动时缓存这个值的EA被市场收割了。
缺陷三:OrderSelect()的状态腐化陷阱
这个Bug我追了一整周。
你在循环遍历未平仓订单。选中一个订单,取到ticket,然后调用OrderModify()。接着你去选另一个订单。但有时候,你刚改完的那个订单因为被成交或平仓,已经从交易池里消失了。当你再次尝试选它时,OrderSelect()失败,但你没检查返回值。
`mql4
// ❌ 问题:OrderSelect()失败未检查
for(int i = OrdersTotal() - 1; i >= 0; i--) {
OrderSelect(i, SELECT_BY_POS);
int ticket = OrderTicket();
// ... 对ticket做点什么 ...
}
// ✅ 修复:永远检查OrderSelect()返回值
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(!OrderSelect(i, SELECT_BY_POS)) {
Print("OrderSelect失败,索引 ", i, " | 错误: ", GetLastError());
continue; // 不要假设订单存在
}
int ticket = OrderTicket();
// ... 对ticket做点什么 ...
}
`
但真正致命的是——当你结合持仓修改时,这个问题会加倍放大。 我修过一个对冲EA,它在一个数组里跟踪订单ticket。修改止损后,EA假设这个ticket仍然有效。然后一个部分成交出现了(订单在一侧成交了但另一侧没成),ticket在循环中途失效了。
`mql4
// ✅ 修复:用ticket跟踪订单,且每次操作前验证
int modifiedTickets[]; // 存储已修改订单的ticket
void TrackAndModifyOrders() {
ArrayResize(modifiedTickets, 0);
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(!OrderSelect(i, SELECT_BY_POS)) continue;
int ticket = OrderTicket();
// 检查这个订单是否已被修改过
bool alreadyModified = false;
for(int j = 0; j < ArraySize(modifiedTickets); j++) {
if(modifiedTickets[j] == ticket) {
alreadyModified = true;
break;
}
}
if(alreadyModified) continue; // 跳过已修改的订单
// 尝试修改
if(OrderModify(ticket, ...)) {
// 存储ticket,防止重复修改
int size = ArraySize(modifiedTickets);
ArrayResize(modifiedTickets, size + 1);
modifiedTickets[size] = ticket;
}
}
}
`
参考来源: 官方论坛上有开发者确认,ticket验证是防止数组越界和状态错乱的关键。
缺陷四:OrderModify()的双击陷阱
又一个容易踩的坑:如果新的SL/TP和当前值完全相同,OrderModify()会失败。不是"价格无效",而是"没有变化"。
`mql4
// ❌ 问题:修改为相同值会触发错误1(无错误码)
double currentSL = OrderStopLoss();
double newSL = currentSL; // 和当前一样
OrderModify(ticket, OrderOpenPrice(), newSL, tp, 0);
// 静默失败,因为没有任何变化需要提交
`
修复很简单但常被忽略:
`mql4
// ✅ 修复:先判断是否真的需要修改
double currentSL = OrderStopLoss();
double currentTP = OrderTakeProfit();
if(MathAbs(newSL - currentSL) < Point && MathAbs(newTP - currentTP) < Point) {
// 无需修改——跳过OrderModify调用
Print("SL/TP未变,跳过修改。");
return;
}
OrderModify(ticket, OrderOpenPrice(), newSL, newTP, 0);
`
缺陷五:终端重启导致的状态丢失
这是最严重的结构性缺陷。大多数EA把所有状态存在运行时变量里——终端一重启,这些变量全归零。
我修过一个马丁格尔EA,它用static int变量记录倍数。周五晚上,它有一个亏损链挂在那里。周末,VPS重启了。周一早上,EA重新启动,倍数重置为1——而亏损持仓还在账户里。它开了一个基础手数的单子,市场反向一走,账户直接爆掉。
解决方案是两层架构:持久化关键状态 + 启动时与券商对账。
`mql4
// ✅ 修复:状态写入文件 + 启动时与券商对账
string stateFile = "ea_state_" + IntegerToString(Magic) + ".bin";
void SaveState() {
int handle = FileOpen(stateFile, FILE_WRITE|FILE_BIN);
if(handle != INVALID_HANDLE) {
FileWriteInteger(handle, gridLevel);
FileWriteInteger(handle, cycleCount);
FileWriteDouble(handle, averageEntry);
FileClose(handle);
}
}
void LoadAndReconcileState() {
int handle = FileOpen(stateFile, FILE_READ|FILE_BIN);
if(handle != INVALID_HANDLE) {
gridLevel = FileReadInteger(handle);
cycleCount = FileReadInteger(handle);
averageEntry = FileReadDouble(handle);
FileClose(handle);
}
// 关键:和券商实际状态对账
int actualPositions = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--) {
if(OrderSelect(i, SELECT_BY_POS)) {
if(OrderMagicNumber() == Magic && OrderSymbol() == Symbol()) {
actualPositions++;
}
}
}
// 如果内部状态和现实不符,以券商为准
if(actualPositions != gridLevel) {
Print("状态不一致: 内部=", gridLevel, " 实际=", actualPositions);
gridLevel = actualPositions; // 券商永远是真理
}
}
int OnInit() {
LoadAndReconcileState();
return INIT_SUCCEEDED;
}
`
参考来源: MQL4官方文档确认,每个EA运行在独立线程中,终端重启不会保留状态。依赖static变量存储关键状态是公认的反模式。
规律总结
这五个缺陷的根源完全相同:EA只在策略测试器里测过——那个所有操作都能成功的地方。订单瞬时成交。终端从不重启。品种属性和开发者环境一致。历史数据完整。
实盘是故障发生的环境。生产级代码必须假设:每个交易请求都可能被拒绝,每个存储的值都可能丢失,关于券商的每个假设都可能出错。
检验任何EA的五个问题:
它在每个 OrderSend、OrderModify、OrderClose上都检查返回值吗?
它在每次修改前都对照 MODE_STOPLEVEL校验SL/TP吗?
它在每个循环里都检查 OrderSelect()返回值吗?
它在调用 OrderModify()`前确认值确实发生了变化吗?任何一个答案是"否",回测结果就是迟早要炸的定时炸弹。
本文首发于FXEAR.com,原创内容,未经授权禁止转载。