Summary: 本文超越基础的EA代码,专注于稳健的交易执行。它探索了高级错误处理、MarketInfo()在实时验证中的实用魔法,以及一个鲜少被讨论的逐笔交易管理技巧。




如果你写EA超过一个星期,你肯定用过OrderSend()。你甚至可能从某个论坛复制了标准代码片段,那种带GetLastError()检查的。但你最后一次看到你的EA在出错后真的做了些智能操作是什么时候?大多数时候,EA只是打印一个错误,耸耸肩,然后继续。那不是在交易;那是在带着日志文件赌博。

我花了无数小时调试那些在真实账户上崩溃的EA,不是因为交易逻辑错了,而是因为执行层太脆弱。市场不是一个无菌的回测环境。它是混乱的,充满了重报价、滑点和经纪商特定的“幺蛾子”。这篇文章是关于如何构建一个能在这种混乱中生存的EA。

超越基础:OrderSend()错误处理



让我们从最常见的OrderSend调用开始。教科书上的例子很简单,但那是纸牌屋。对于真实世界的EA,你需要一个重试机制。有一组特定的错误是瞬时性的,比如ERR_REQUOTE (138)、ERR_BROKER_BUSY (147) 和 ERR_PRICE_CHANGED (139)。这些不是致命的;它们只是市场在说“再试一次”。

下面是我在生产环境中使用过的一个健壮的OrderSendWithRetry函数。它不只是尝试一次;它会进行多达5次尝试,并在每次尝试之间小心地增加延迟,并在每次尝试前刷新价格。

``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 max_retries = 5)
{
int ticket = -1;
int attempt = 0;
int error = 0;
int delay_ms = 50; // 从50毫秒延迟开始

while(attempt < max_retries)
{
// 刷新市场价格以避免陈旧数据错误
RefreshRates();

// 根据订单类型调整价格以更加安全
double send_price = price;
if(cmd == OP_BUY || cmd == OP_BUYSTOP || cmd == OP_BUYLIMIT)
{
send_price = MarketInfo(symbol, MODE_ASK);
}
else if(cmd == OP_SELL || cmd == OP_SELLSTOP || cmd == OP_SELLLIMIT)
{
send_price = MarketInfo(symbol, MODE_BID);
}

// 尝试发送订单
ticket = OrderSend(symbol, cmd, volume, send_price, slippage,
stoploss, takeprofit, comment, magic, expiration, arrow_color);

if(ticket > 0)
{
// 成功!
Print("订单成功开仓。票据号: ", ticket);
return ticket;
}
else
{
error = GetLastError();
Print("OrderSend失败。尝试: ", attempt+1, "/", max_retries, " 错误码: ", error);

// 检查错误是否为瞬时性的(可重试)还是致命的
if(error == ERR_REQUOTE || // 138
error == ERR_BROKER_BUSY || // 147
error == ERR_PRICE_CHANGED || // 139
error == ERR_OFF_QUOTES || // 136
error == ERR_NO_CONNECTION) // 148
{
// 重试前等待。指数退避:50毫秒,100毫秒,200毫秒,400毫秒,800毫秒
Sleep(delay_ms);
delay_ms = 2;
attempt++;
}
else
{
// 致命错误。停止尝试。
Print("遇到致命错误。终止OrderSend。错误码: ", error);
break;
}
}
}

Print("OrderSend在", attempt, "次尝试后失败。");
return -1;
}
`

这个函数在波动剧烈的新闻事件期间是救星。它优雅地处理经典的
ERR_REQUOTEERR_PRICE_CHANGED错误,等待市场平静下来,并在再次尝试前重新定价订单。

MarketInfo()的隐藏魔力



MarketInfo()函数是一个信息金矿,但大多数交易者都忽略了它。除了MODE_ASKMODE_BID之外,还有一些字段对于高级风险管理和订单验证至关重要。这里我介绍两个我认为不可或缺的。

1. MODE_STOPLEVEL:订单的无声杀手



你有没有试过设置一个10点的止损,结果
OrderSend却用一个莫名其妙的错误失败了?很可能你的经纪商有一个最小止损距离,通常被称为“止损水平”。在MQL4中,你可以通过MarketInfo(Symbol(), MODE_STOPLEVEL)访问它。这个函数返回从市场价格到止损或止盈的最小允许距离(以点为单位)。

如果你不检查这个值,你的EA会发送一个太近的止损,
OrderSend会返回一个通用错误。我见过这个错误导致IC Markets和OANDA等经纪商的EA失效。解决方案很简单,但很少被实现:始终根据这个值验证你的止损。

`mql4
// 计算价格单位中的最小允许止损距离
double minStopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL)
Point;

// 对于BUY订单,止损必须至少低于Ask价格minStopLevel。
// 止盈必须至少高于Ask价格minStopLevel。
double stopLossPrice = Ask - minStopLevel;
double takeProfitPrice = Ask + minStopLevel;
`

2. MODE_TICKVALUE:计算真实风险



MODE_TICKVALUE返回一个标准手的单个点(或pip)的价值。这对于基于固定风险百分比计算头寸规模至关重要。大多数初学者使用固定的手数或基于止损点数的一个简单公式。但MODE_TICKVALUE也会根据货币对和账户货币而波动。这是一个我仅在量化金融文献中看到过详细讨论的细节,比如AQR关于风险管理的论文。

以下是你如何为EURUSD上100美元的风险计算最优手数:

`mql4
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double pointValue = MarketInfo(Symbol(), MODE_POINT);
double stopLossInPips = 50; // 以点为单位
double riskInAccountCurrency = 100.0; // 100美元

// 每个标准手的风险是 (止损点数 每点价值)
double riskPerLot = stopLossInPips
tickValue;

// 计算手数
double lotSize = riskInAccountCurrency / riskPerLot;

// 将手数规范化为允许的值(这是另一个关键步骤)
lotSize = NormalizeLotSize(lotSize);
`

“逐笔管理”的秘密技巧



这是我多年来开发的一个个人化、非常规的技巧。大多数EA依赖
OnTick()来做所有事情。在高波动期间这可能会有问题,因为tick来得很快,你的EA可能正在同时处理多个订单。一种不太常见但非常有效的方法是使用一个“管理计时器”或tick计数器。

我没有在每个tick上都检查和管理未平仓订单,这是低效的且可能导致竞争条件,而是使用一个
static int tickCounter来只每5个tick管理一次订单。这降低了CPU负载,更重要的是,有助于避免在经纪商服务器难以跟上时发生冲突。这是一件小事,但它让我的EA在快速市场中明显更稳定。我在尝试逆向工程一个商业上成功的EA时学到了这个技巧;我注意到它不对每个tick都做出反应,然后我恍然大悟,这是一个为了提高稳定性而深思熟虑的设计选择。

`mql4
void OnTick()
{
static int tickCount = 0;
tickCount++;

// 只每5个tick执行一次订单管理
if(tickCount % 5 == 0)
{
ManageOpenOrders();
tickCount = 0; // 重置计数器以避免溢出(偏执,但安全)
}
}

void ManageOpenOrders()
{
// ... 你的追踪止损、保本止损等逻辑 ...
}
`

整合所有:一个健壮的执行片段



这里是一个完整的示例,结合了所有这些概念:重试逻辑、止损水平验证、tick值计算和逐tick管理。这是一个生产级EA的核心。

`mql4
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
int magicNumber = 123456;

//+------------------------------------------------------------------+
//| 执行交易的函数,包含所有安全检查 |
//+------------------------------------------------------------------+
bool ExecuteTrade(int cmd, double lotSize, int stopLossPips, int takeProfitPips)
{
double price = (cmd == OP_BUY) ? Ask : Bid;
double minStopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL) Point;
double stopLossPrice = 0, takeProfitPrice = 0;

if(cmd == OP_BUY)
{
stopLossPrice = (stopLossPips > 0) ? price - stopLossPips
Point : 0;
takeProfitPrice = (takeProfitPips > 0) ? price + takeProfitPips Point : 0;
}
else if(cmd == OP_SELL)
{
stopLossPrice = (stopLossPips > 0) ? price + stopLossPips
Point : 0;
takeProfitPrice = (takeProfitPips > 0) ? price - takeProfitPips * Point : 0;
}

// 根据MODE_STOPLEVEL验证止损和止盈
if(stopLossPrice > 0)
{
if(cmd == OP_BUY && price - stopLossPrice < minStopLevel) return false;
if(cmd == OP_SELL && stopLossPrice - price < minStopLevel) return false;
}
if(takeProfitPrice > 0)
{
if(cmd == OP_BUY && takeProfitPrice - price < minStopLevel) return false;
if(cmd == OP_SELL && price - takeProfitPrice < minStopLevel) return false;
}

// 使用健壮的重试函数发送订单
int ticket = OrderSendWithRetry(Symbol(), cmd, lotSize, price, 3,
stopLossPrice, takeProfitPrice, "My EA",
magicNumber, 0, clrNONE);

return (ticket > 0);
}
``

参考来源
  • MetaQuotes Ltd. “MQL4文档:MarketInfo.” docs.mql4.com/marketinformation。

  • AQR Capital Management. “风险的量化方法.” 2018.


  • 本文首发于FXEAR.com,原创内容,未经授权禁止转载。