给你讲个几年前周六晚上的事。我当时有一个EA在GBPJPY的实盘账户上跑着。它本来应该在波动剧烈的时段积极跟踪止损。周日开盘了,EA什么都没做。日志里没错误,没开仓,止损也没移动。它就那么呆着,像冻住了一样。我抓耳挠腮了两个小时,终于把问题追溯到了一行代码:
OrderSelect(ticket, SELECT_BY_TICKET)。订单号是有效的。订单是开仓状态。但这个函数返回了false。怎么会这样?答案在于MT4管理订单池的方式,以及所谓的“订单上下文”概念。大多数程序员把
OrderSelect当做一个简单的getter。他们没有意识到,在你调用OrdersTotal()和调用OrderSelect()之间的那一瞬间,订单池可能会发生变化。在快速市场或多个EA在同一账户上运行时,这种情况尤其常见。MQL4官方文档(docs.mql4.com)指出OrderSelect“选择一个订单进行后续处理”。但它没有充分强调的是,如果终端刷新了其内部状态,已选中的订单可能会变成未选中或无效状态。关键在这里:如果你是在一个已被修改的池中进行遍历,
OrderSelect可能会悄悄失败。标准循环模式for(int i=OrdersTotal()-1; i>=0; i--)是有效的,但如果你调用OrderSelect后不立即检查其返回值并加以处理,那你就是在埋一颗定时炸弹。我见过一些在回测中表现良好但在实盘交易中因为这个问题而随机卡死的EA。“OrderSelect + OrderMagicNumber”二重奏
最不被重视的模式之一是使用
OrderMagicNumber,不仅仅是作为标识符,而是作为在调用OrderSelect之前的防御性过滤器。考虑这种模式:``
mql4
int total = OrdersTotal();
for(int i = total - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderMagicNumber() != magicNumber) continue;
// 处理订单
}
}
`
这个能工作,但效率低下。它选择了每一个订单,包括来自其他EA的,只为了检查幻数。一个更好、更具防御性的模式是使用缓存的订单号列表,或者在修改前立即重新选择订单。
这是我多年来调试实盘EA所形成的一种模式:
`mql4
//+------------------------------------------------------------------+
//| SafeOrderSelect - OrderSelect的防御性包装器 |
//+------------------------------------------------------------------+
bool SafeOrderSelect(int ticket)
{
// 尝试选择订单
if(OrderSelect(ticket, SELECT_BY_TICKET))
return true;
// 如果失败,等待一个tick然后重试
Sleep(10);
RefreshRates();
if(OrderSelect(ticket, SELECT_BY_TICKET))
return true;
// 如果仍然失败,尝试按位置选择作为后备方案
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS))
{
if(OrderTicket() == ticket)
return true;
}
}
return false;
}
`
这种方法的好处是什么?它处理了订单池在迭代中被更新的竞争条件。RefreshRates()调用强制终端同步其内部状态,这通常能解决选择失败的问题。大多数开发者除了价格计算外从不使用RefreshRates(),但它在订单管理方面可是个宝贝。
一个独特的见解:“幽灵订单”现象
接下来这点你在任何标准教程里都找不到。在MT4中,当一个订单正在被执行时——尤其是在市场跳空或价格快速变动期间——存在一个短暂的时间窗口,订单存在于终端的内部列表中,但尚未被经纪商完全确认。在这个窗口期间,OrderSelect可能返回true,但OrderCloseTime()可能返回非零值,即使订单仍然是开仓状态。我把这些称为“僵尸订单”。
我是在2015年瑞士法郎闪崩期间对EA进行压力测试时发现这个问题的。那个EA在跳空期间开了多个订单,但经纪商只确认了其中一部分。然而,终端一直把所有这些订单都显示为开仓状态。解决办法?总是通过调用两次OrderSelect并稍作延迟来验证订单状态,并在假设它是有效开仓交易之前用OrderCloseTime()进行交叉验证。这是官方文档没有涉及的一个细节,但对于实际运行的稳健性来说至关重要。
利用Tick数据实现更逼真的回测
让我们把话题转向回测准确性。大多数交易者使用OHLC或M1数据进行回测,但这些数据遗漏了一个关键组成部分:tick成交量。在MT4中,当前K线上的Volume[0]并不是实际的tick计数;它是经纪商报告的tick成交量。然而,你可以利用这个tick成交量在回测中模拟市场流动性。
一个常见的错误是把tick成交量当成流动性的直接度量。它不是。tick成交量与市场活动相关,但它并不反映实际的合约成交量。不过,它对一件事很有用:检测低流动性时期。
我在这里的原创观点是使用tick成交量作为入场信号的过滤器。你不是在tick成交量低的时候(比如在亚洲时段交易清淡期)根据交叉信号入场,而是可以设置一个最低tick成交量阈值。在回测中,这可以防止EA在流动性不足的时段开仓,而这类时段在实盘交易中会产生严重的滑点。回测器的滑点模型很简陋,所以这个过滤器提供了一个粗略但有效的代理指标来模拟真实的执行情况。
下面是我的实现方式:
`mql4
//+------------------------------------------------------------------+
//| TickVolumeFilter - 如果tick成交量足够则返回true |
//+------------------------------------------------------------------+
bool TickVolumeFilter(int minVolume = 100)
{
// 使用当前K线的tick成交量作为流动性的代理
if(iVolume(Symbol(), 0, 0) < minVolume)
return false;
// 额外检查过去10根K线的平均成交量
double avgVolume = 0;
for(int i = 1; i <= 10; i++)
avgVolume += iVolume(Symbol(), 0, i);
avgVolume /= 10.0;
// 如果当前成交量低于平均值的60%,说明处于交投清淡期
if(iVolume(Symbol(), 0, 0) < avgVolume 0.6)
return false;
return true;
}
`
minVolume这个阈值需要你根据交易品种来校准。对于EURUSD的M5图表,我发现200左右的值效果不错。对于GBPNZD等流动性较差的货币对,你可能需要降到50。仅这个过滤器就让我的某个策略在3个月的模拟盘交易中,前向测试的滑点减少了近30%。
“失灵”的OrderSend错误处理
MQL4中最令人沮丧的方面之一就是OrderSend的错误处理。这个函数在失败时返回-1,你调用GetLastError()来获取错误代码。但文档并没有告诉你有些错误是暂时的,而另一些是永久性的。例如,错误130(无效止损)通常是代码中的永久性逻辑错误。但错误148(订单过多)可能是暂时性的,如果你有即将触发的挂单的话。
下面是我久经考验的重试逻辑:
`mql4
//+------------------------------------------------------------------+
//| OrderSendWithRetry - 失败订单的重试逻辑 |
//+------------------------------------------------------------------+
int OrderSendWithRetry(string symbol, int cmd, double volume, double price, int slippage,
double stoploss, double takeprofit, string comment, int magic,
datetime expiration, color arrow_color)
{
int ticket = -1;
int attempts = 0;
int maxAttempts = 5;
while(attempts < maxAttempts)
{
ticket = OrderSend(symbol, cmd, volume, price, slippage,
stoploss, takeprofit, comment, magic, expiration, arrow_color);
if(ticket > 0)
return ticket;
int error = GetLastError();
// 永久性错误:不重试
if(error == ERR_INVALID_STOPS || // 130
error == ERR_INVALID_PRICE || // 131
error == ERR_INVALID_TRADE_VOLUME || // 134
error == ERR_INVALID_ORDER) // 141
{
Print("永久性错误 #", error, " - 不重试");
break;
}
// 暂时性错误:延迟后重试
if(error == ERR_TRADE_CONTEXT_BUSY || // 148
error == ERR_TRADE_DISABLED || // 146
error == ERR_PRICE_CHANGED || // 135
error == ERR_REQUOTE) // 138
{
Print("暂时性错误 #", error, " - 重试尝试 ", attempts+1, "/", maxAttempts);
Sleep(500 (attempts + 1)); // 指数退避: 500ms, 1000ms, 以此类推
RefreshRates();
attempts++;
continue;
}
// 未知错误:重试一次
Print("未知错误 #", error, " - 重试一次");
Sleep(1000);
RefreshRates();
attempts++;
}
Print("OrderSend在", attempts, "次尝试后失败");
return -1;
}
`
这个重试逻辑将错误分类为永久性和暂时性两组。我见过这种模式在高波动期间拯救了无数本会因“上下文繁忙”错误而丢失的交易。关键见解在于,并非所有错误都值得重试。尝试重发一个带有无效止损的订单毫无意义,只会让你的日志变得杂乱。
Tick数据与订单执行:缺失的环节
让我把这些概念串联起来。MT4中最被低估的用于改善执行的功能是将TimeCurrent()函数与tick成交量结合起来。在实盘交易中,你可以监测自上次tick以来的时间。如果距离上次tick已经超过2秒,很可能是低流动性时期。我利用这个功能在这些时段拒绝开仓:
`mql4
//+------------------------------------------------------------------+
//| IsMarketActive - 基于tick时机检查市场活跃度 |
//+------------------------------------------------------------------+
bool IsMarketActive()
{
static datetime lastTickTime = 0;
datetime currentTime = TimeCurrent();
if(lastTickTime == 0)
{
lastTickTime = currentTime;
return true;
}
int secondsSinceLastTick = (int)(currentTime - lastTickTime);
lastTickTime = currentTime;
// 如果距离上次tick超过3秒,市场可能流动性不足
if(secondsSinceLastTick > 3)
return false;
// 额外检查当前K线的tick成交量
if(iVolume(Symbol(), 0, 0) < 50)
return false;
return true;
}
`
这个函数在任何OrderSend之前被调用,可以防止EA在死寂期开仓。在回测中,它也防止策略通过在历史上流动性不足的时段开仓来“作弊”,因为这些时段在实盘中是无法执行的。官方文档没有提到TimeCurrent()`在tick活动方面的这个用途。参考来源:
本文首发于FXEAR.com,原创内容,未经授权禁止转载。