那个只在实盘出现的Bug
我见过太多次了。一个EA回测曲线漂亮得像教科书,逻辑清晰,入场条件干净,出场规则合理。放到模拟盘跑,没问题。一上实盘,
OrderSend开始疯狂报错130。第一反应永远是:“经纪商在搞我。”但真正的问题很少出在经纪商身上。
问题通常藏在一行代码里,大部分MQL4开发者从教程里复制过来之后再也没想过它:
``
mql4
double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL);
`
看起来人畜无害。MQL4官方文档说MODE_STOPLEVEL返回“止损水平(以点为单位)”。但文档里有一句话,大多数人扫一眼就跳过去了:
“MODE_STOPLEVEL为零值意味着要么对止损/止盈的最小距离没有任何限制,要么交易服务器使用了一些外部机制进行动态水平控制,这些机制无法在客户端终端中翻译。在第二种情况下,GetLastError()可能返回错误130,因为MODE_STOPLEVEL实际上是‘浮动’的。”
再读一遍。文档在明确告诉你:在某些服务器上,MODE_STOPLEVEL返回零,但服务器仍然通过自己的机制强制执行最小止损距离。你的EA看到“无限制”,计算出一个距入场5个点的止损,发送订单,服务器拒绝。
这不是理论上的边缘情况。过去两年里,我在至少三家不同的ECN经纪商上遇到过这个问题。症状永远一样:OrderSend返回-1,GetLastError()返回130,EA在每一个tick上重试同一个无效的止损水平。
大多数人修错的方向
常见的“修复”是硬编码一个最小距离:
`mql4
double stopLevel = MathMax(MarketInfo(Symbol(), MODE_STOPLEVEL), 10 Point);
`
如果你选的数字恰好大于服务器暗中执行的那个值,这能凑合用。但这是创可贴,不是解决方案。你选10个点,经纪商改成15。你选15,波动大的时候点差扩大,你还是会收到重新报价。
我用的方法分两步:先看MODE_STOPLEVEL是否可用;如果是零,回退到基于点差的动态缓冲。
`mql4
//+------------------------------------------------------------------+
//| 返回有效的止损最小距离(价格形式) |
//| 处理某些经纪商上的“浮动STOPLEVEL”问题 |
//+------------------------------------------------------------------+
double GetEffectiveStopLevel()
{
double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL);
// 如果STOPLEVEL为零,服务器可能仍暗中执行一个最小值。
// 回退到基于点差的缓冲:3倍当前点差,最低20个点。
if(stopLevel <= 0)
{
double spread = MarketInfo(Symbol(), MODE_SPREAD);
stopLevel = MathMax(spread 3.0, 20.0);
}
// 在计算值之上再加20%的安全边际。
return stopLevel * 1.2;
}
`
逻辑是这样的:当MODE_STOPLEVEL没用时,点差是你判断“什么算太近”的最佳代理。三倍点差加安全边际,在我测试过的所有经纪商上都管用。不优雅,但稳。
OrderSend在到达服务器之前就已经失败了
文档提到但没有足够强调的一件事:OrderSend在发送请求之前就会验证你的止损和止盈水平。如果水平无效,函数立即返回-1,根本到不了经纪商的服务器。
这对调试很重要。如果你在记录错误并反复看到130,问题可能根本不在服务器。可能是你的EA计算出了一个MQL4本身认为无效的止损水平——基于它确实拥有的MODE_STOPLEVEL值。
文档说得很明确:
“止损和止盈水平不能离市场太近。止损水平的最小距离(以点为单位)可以通过使用MODE_STOPLEVEL参数的MarketInfo()函数获取。在止损水平错误或未标准化的情况下,会生成错误130(ERR_INVALID_STOPS)。”
所以错误可以来自两个地方:客户端终端的验证,或者服务器的拒绝。修复方式一样,但知道是哪一个在拒绝你,有助于校准你的安全边际。
我实际使用的订单执行包装函数
我不直接调用OrderSend,而是把它包在一个函数里,处理标准化、止损水平验证和错误重试。核心代码如下:
`mql4
//+------------------------------------------------------------------+
//| 稳健的MQL4订单开仓包装函数 |
//| 成功返回ticket号,失败返回-1 |
//+------------------------------------------------------------------+
int SafeOrderSend(int cmd, double lots, double price, int slippage,
double slPrice, double tpPrice, string comment, int magic)
{
// 将价格标准化到正确的小数位数
int digits = (int)MarketInfo(Symbol(), MODE_DIGITS);
price = NormalizeDouble(price, digits);
slPrice = NormalizeDouble(slPrice, digits);
tpPrice = NormalizeDouble(tpPrice, digits);
// 根据有效最小止损距离验证止损水平
double minStop = GetEffectiveStopLevel();
double stopDist = minStop Point;
if(cmd == OP_BUY)
{
if(slPrice > 0 && (price - slPrice) < stopDist)
slPrice = NormalizeDouble(price - stopDist, digits);
if(tpPrice > 0 && (tpPrice - price) < stopDist)
tpPrice = NormalizeDouble(price + stopDist, digits);
}
else if(cmd == OP_SELL)
{
if(slPrice > 0 && (slPrice - price) < stopDist)
slPrice = NormalizeDouble(price + stopDist, digits);
if(tpPrice > 0 && (price - tpPrice) < stopDist)
tpPrice = NormalizeDouble(price - stopDist, digits);
}
// 尝试发送订单
int ticket = OrderSend(Symbol(), cmd, lots, price, slippage,
slPrice, tpPrice, comment, magic, 0, clrNONE);
if(ticket < 0)
{
int err = GetLastError();
Print("OrderSend失败。错误码: ", err,
" | SL: ", slPrice, " TP: ", tpPrice,
" | minStop: ", minStop);
}
return ticket;
}
`
注意我是把SL/TP价格向上或向下调整以满足最小距离,而不是拒绝订单。这是一个设计选择。有些开发者倾向于如果止损放不到想要的位置就跳过这笔交易。我倾向于用略微调整的止损开仓,因为大多数情况下差异只有几个点,交易逻辑本身仍然成立。
被忽视的MODE_FREEZELEVEL
还有第二个更隐蔽的问题没有得到足够关注:冻结水平。
MODE_FREEZELEVEL在文档中的描述是:“订单冻结水平(以点为单位)。如果执行价格在冻结水平定义的范围内,订单不能被修改、取消或关闭。”
实际含义:你有一个持仓,价格接近止盈,你想把止损移到盈亏平衡点。你调用OrderModify。它静默失败(或者返回一个你不屑于检查的错误,因为“只是个移动止损而已”)。原因是当前价格离止盈太近,服务器在该区域冻结了修改操作。
我见过一些EA每个tick都在移动止损,从不检查MODE_FREEZELEVEL。它们浪费CPU周期和API调用,偶尔触发经纪商的频率限制警告。修复很简单:
`mql4
// 调用OrderModify做移动止损前,检查冻结水平
double freezeLevel = MarketInfo(Symbol(), MODE_FREEZELEVEL) Point;
if(MathAbs(Bid - OrderTakeProfit()) < freezeLevel)
{
// 离止盈太近,本tick跳过修改
return;
}
`
对OrderSelect数据“新鲜度”的一个非常规看法
关于这个我跟其他开发者争论过:MQL4文档说OrderSelect会把订单数据复制到程序环境中,之后调用OrderOpenPrice()、OrderStopLoss()等返回的是复制的数据,不是实时数据。
文档接着说:“强烈建议在请求订单数据之前调用OrderSelect()函数。”
大多数人把这理解为“读订单属性前总是先调OrderSelect”。这没错。但非常规的推论是:如果你在一个循环里读取订单属性,你得到的是最后一次OrderSelect调用时的快照,不是当前状态。
在快速变动的市场中,这很重要。如果你遍历10个持仓,逐个选中并检查利润,到第十个的时候第一个订单的数据可能已经过了50毫秒。对于移动止损和盈亏平衡逻辑,这个延迟可能是锁定利润和吐回利润之间的差别。
我的做法:对于任何订单修改决策,我在OrderModify调用之前立即再调一次OrderSelect,即使我在函数的更早位置已经选过这个订单。冗余换来的是安全。
`mql4
// 不要信任循环迭代时的数据。
// 在修改之前立即重新选择。
if(OrderSelect(ticket, SELECT_BY_TICKET))
{
double currentSL = OrderStopLoss();
double newSL = CalculateNewStopLoss();
if(newSL != currentSL) // 只在确实不同时才修改
{
bool result = OrderModify(ticket, OrderOpenPrice(), newSL,
OrderTakeProfit(), 0, clrNONE);
if(!result)
Print("修改失败: ", GetLastError());
}
}
`
newSL != currentSL这个检查是为了避免错误1(ERR_NO_RESULT),文档对此有明确警告:“如果作为函数参数传递未更改的值,会生成错误1(ERR_NO_RESULT)。”
真正的教训
MQL4的交易函数看起来很简单。OrderSend有11个参数,大部分可以用默认值。文档很详尽。但“文档这么说”和“经纪商实际这么做”之间的鸿沟,正是EA出问题的地方。
MODE_STOPLEVEL浮动问题是最清晰的例子。文档警告过你,但方式很容易被忽略。修复并不复杂——只需要读到那一句话,然后在构建订单执行逻辑时把“客户端终端没有关于服务器端约束的完整信息”这个可能性考虑进去。
写好你的包装函数。标准化你的价格。检查你的止损水平。还有,务必在每个交易函数调用之后检查GetLastError()`。参考来源:
本文首发于FXEAR.com,原创内容,未经授权禁止转载。