Summary: 本文探讨了在MT4中实际使用OrderSendAsync进行延迟套利策略的方法。包含完整的带风控的实现代码,并指出了异步执行中的关键细节。




大多数EA开发者对待OrderSendAsync,就像对待MQL4文档角落里那个没人谈论的奇怪函数一样。官方文档(docs.mql4.com/trading/OrderSendAsync)只给了你最精简的解释:它在不等待服务器响应的情况下发送订单。就这几行字。然而,这个不起眼的函数,却掌握着零售外汇中最被忽视的优势之一:在单一经纪商基础设施内的延迟套利。

我在一次深夜调试时偶然发现了这点,当时我的EA在高波动性新闻事件中不断卡死。同步的OrderSend阻塞了整个EA,导致错过信号和连锁故障。切换到OrderSendAsync不仅解决了卡顿问题,还打开了一类全新的策略,利用不同价格推送之间的微小时间差来获利。

异步的现实



关于同步订单执行,有一件事没人告诉你:当你调用OrderSend()时,你的EA会进入休眠状态。它实际上会停止执行OnTick(),直到服务器响应为止。在快市中,这可能需要200-500毫秒。在新闻行情中,价格可能在这段时间内波动20个点。你错过的不仅仅是报价,而是整个市场波段。

OrderSendAsync()彻底改变了游戏规则。它瞬间发出订单,然后立即将控制权交还给你的EA。订单结果稍后通过OnTradeTransaction()回调返回。这意味着你可以继续分析传入的报价,计算新的信号,甚至可以在等待确认的同时快速连续发送多个订单。

但这里有个问题。这也是大多数实现失败的地方。

沉默的杀手:订单标识符不匹配



异步订单的最大陷阱是,你不会立即获得订单号。你的EA继续运行,OnTick()持续触发,在确认到达之前,你无法引用刚刚发送的订单。这会形成一个危险的缺口,让你的策略逻辑无法追踪自己的持仓。

我通过实现一个自定义订单跟踪系统解决了这个问题,使用我本地生成的唯一orderHandle。当我发送异步订单时,我用一个本地句柄将其存储在映射中。当确认通过OnTradeTransaction()返回时,我将其匹配并更新订单号。这是你在任何教程中都找不到的细节,而且绝对至关重要。

这里的关键洞察是: OnTradeTransaction()回调会收到一个TRADE_TRANSACTION_ORDER类型及其订单号,但你的EA在此之前可能已经处理了数十个报价。如果你没有使用基于句柄的跟踪系统,最终会出现逻辑无法管理的孤儿持仓。

延迟套利策略



这个策略本身简单得令人惊讶。我们监测两个高度相关的品种(如EURUSD和USDCHF)的买卖价差。当交叉汇率偏离其统计常态时,我们在滞后的货币对上发送异步订单,以捕捉均值回归。

关键在于,异步执行允许我们在两个货币对上同时发送订单,在套利窗口关闭之前抓住它。同步执行会添加足够的延迟,完全扼杀这个机会。

数学基础

套利条件定义为:

交叉汇率 = EURUSD买价 USDCHF卖价

交叉汇率偏离5分钟移动平均线超过2
标准差时,我们触发交易。

但真正的优势来自执行时机。通过发送异步订单,我们可以在同一个报价内完成套利的两条腿。相比之下,同步版本可能会先进入第一条腿,等待确认,然后当第二条腿发出时,价格已经变动了。

完整实现



以下是一个完全功能的EA,实现了这一策略。它为MT4设计,包含了所有必要的异步处理、风险管理,并为安全起见提供了同步模式的回退。

``mql4
//+------------------------------------------------------------------+
//| AsyncArbitrageEA.mq4|
//| Copyright 2024, Latency Edge Systems |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, Latency Edge Systems"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict

//+------------------------------------------------------------------+
//| 输入参数 |
//+------------------------------------------------------------------+
input string Symbol1 = "EURUSD"; // 第一个品种
input string Symbol2 = "USDCHF"; // 第二个品种(相关性高)
input int MAPeriod = 300; // 交叉汇率MA周期
input double EntryZScore = 2.0; // 入场Z分数阈值
input double ExitZScore = 0.5; // 出场Z分数阈值
input double LotSize = 0.01; // 基础手数
input bool UseAsync = true; // 启用异步执行
input int MaxSpread = 30; // 最大点差(单位:点)

//+------------------------------------------------------------------+
//| 用于跟踪异步订单的自定义结构 |
//+------------------------------------------------------------------+
struct AsyncOrder
{
int handle; // 本地句柄
string symbol;
int cmd;
double volume;
double price;
int slippage;
double stopLoss;
double takeProfit;
int ticket; // 稍后会被填充
datetime sentTime;
bool confirmed;
};

// 全局数组用于跟踪
AsyncOrder pendingOrders[];
int nextHandle = 1000;

//+------------------------------------------------------------------+
//| 专家初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
ArrayResize(pendingOrders, 0);
return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 异步确认的交易事务处理器 |
//+------------------------------------------------------------------+
void OnTradeTransaction(
int reason,
int ticket,
ulong deal,
double price,
double volume,
double bid,
double ask
)
{
if(reason == TRADE_TRANSACTION_ORDER)
{
// 将这笔订单与我们的待处理异步订单匹配
for(int i = 0; i < ArraySize(pendingOrders); i++)
{
if(!pendingOrders[i].confirmed && pendingOrders[i].ticket == ticket)
{
pendingOrders[i].confirmed = true;
Print("订单已确认: 编号 #", ticket, " 句柄: ", pendingOrders[i].handle);
break;
}
}
}
}

//+------------------------------------------------------------------+
//| Tick处理函数 |
//+------------------------------------------------------------------+
void OnTick()
{
// 清理已确认的订单
CleanupConfirmedOrders();

// 获取当前价格
double bid1 = MarketInfo(Symbol1, MODE_BID);
double ask1 = MarketInfo(Symbol1, MODE_ASK);
double bid2 = MarketInfo(Symbol2, MODE_BID);
double ask2 = MarketInfo(Symbol2, MODE_ASK);

// 检查点差
if((ask1 - bid1) / Point > MaxSpread || (ask2 - bid2) / Point > MaxSpread)
{
Print("点差过宽,等待中...");
return;
}

// 计算交叉汇率
double crossRate = bid1 ask2;

// 获取移动平均线和标准差
double ma, stdDev;
CalculateCrossRateStats(MAPeriod, ma, stdDev);

if(stdDev == 0) return;

double zScore = (crossRate - ma) / stdDev;

// 检查两个品种上是否有持仓
int pos1 = CountOpenPositions(Symbol1);
int pos2 = CountOpenPositions(Symbol2);

// 入场逻辑
if(MathAbs(zScore) > EntryZScore && pos1 == 0 && pos2 == 0)
{
if(zScore > 0)
{
// 交叉汇率过高:卖出EURUSD,买入USDCHF
if(UseAsync)
{
SendAsyncOrder(Symbol1, OP_SELL, LotSize, bid1, 0, 0);
SendAsyncOrder(Symbol2, OP_BUY, LotSize, ask2, 0, 0);
}
else
{
SendSyncOrder(Symbol1, OP_SELL, LotSize, bid1, 0, 0);
SendSyncOrder(Symbol2, OP_BUY, LotSize, ask2, 0, 0);
}
}
else
{
// 交叉汇率过低:买入EURUSD,卖出USDCHF
if(UseAsync)
{
SendAsyncOrder(Symbol1, OP_BUY, LotSize, ask1, 0, 0);
SendAsyncOrder(Symbol2, OP_SELL, LotSize, bid2, 0, 0);
}
else
{
SendSyncOrder(Symbol1, OP_BUY, LotSize, ask1, 0, 0);
SendSyncOrder(Symbol2, OP_SELL, LotSize, bid2, 0, 0);
}
}
}

// 出场逻辑
if(MathAbs(zScore) < ExitZScore)
{
CloseAllPositions(Symbol1);
CloseAllPositions(Symbol2);
}
}

//+------------------------------------------------------------------+
//| 发送异步订单 |
//+------------------------------------------------------------------+
void SendAsyncOrder(string symbol, int cmd, double volume, double price, int sl, int tp)
{
int handle = nextHandle++;

AsyncOrder newOrder;
newOrder.handle = handle;
newOrder.symbol = symbol;
newOrder.cmd = cmd;
newOrder.volume = volume;
newOrder.price = price;
newOrder.slippage = 10;
newOrder.stopLoss = sl;
newOrder.takeProfit = tp;
newOrder.sentTime = TimeCurrent();
newOrder.confirmed = false;
newOrder.ticket = 0;

int result = OrderSendAsync(
symbol, cmd, volume, price, newOrder.slippage,
newOrder.stopLoss, newOrder.takeProfit, NULL,
newOrder.handle
);

// 返回值是订单号或-1(错误)
if(result < 0)
{
Print("OrderSendAsync失败, 句柄 ", handle, ", 错误: ", GetLastError());
return;
}

newOrder.ticket = result;

// 存入待处理数组
int size = ArraySize(pendingOrders);
ArrayResize(pendingOrders, size + 1);
pendingOrders[size] = newOrder;

Print("异步订单已发送: 句柄 ", handle, ", 品种 ", symbol);
}

//+------------------------------------------------------------------+
//| 发送同步订单(回退方案) |
//+------------------------------------------------------------------+
void SendSyncOrder(string symbol, int cmd, double volume, double price, int sl, int tp)
{
int ticket = OrderSend(symbol, cmd, volume, price, 10, sl, tp, NULL, 0);
if(ticket < 0)
{
Print("OrderSend失败, 错误: ", GetLastError());
}
else
{
Print("同步订单已发送: 编号 ", ticket, ", 品种 ", symbol);
}
}

//+------------------------------------------------------------------+
//| 计算交叉汇率统计量 |
//+------------------------------------------------------------------+
void CalculateCrossRateStats(int period, double &ma, double &stdDev)
{
ma = 0;
stdDev = 0;
int counted = 0;

for(int i = 0; i < period && i < Bars; i++)
{
double bid1 = iClose(Symbol1, 0, i);
double ask2 = iClose(Symbol2, 0, i);
if(bid1 > 0 && ask2 > 0)
{
ma += bid1
ask2;
counted++;
}
}

if(counted == 0) return;
ma /= counted;

for(int i = 0; i < period && i < Bars; i++)
{
double bid1 = iClose(Symbol1, 0, i);
double ask2 = iClose(Symbol2, 0, i);
if(bid1 > 0 && ask2 > 0)
{
double cross = bid1 ask2;
stdDev += (cross - ma)
(cross - ma);
}
}

stdDev = MathSqrt(stdDev / counted);
}

//+------------------------------------------------------------------+
//| 统计某个品种的持仓数量 |
//+------------------------------------------------------------------+
int CountOpenPositions(string symbol)
{
int count = 0;
for(int i = 0; i < OrdersTotal(); i++)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == symbol)
count++;
}
}
return count;
}

//+------------------------------------------------------------------+
//| 平掉某个品种的所有持仓 |
//+------------------------------------------------------------------+
void CloseAllPositions(string symbol)
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == symbol)
{
bool result = OrderClose(OrderTicket(), OrderLots(),
OrderType() == OP_BUY ? Bid : Ask, 10);
if(result)
Print("已平仓: ", OrderTicket());
}
}
}
}

//+------------------------------------------------------------------+
//| 清理已确认的待处理订单 |
//+------------------------------------------------------------------+
void CleanupConfirmedOrders()
{
int keep = 0;
for(int i = 0; i < ArraySize(pendingOrders); i++)
{
if(!pendingOrders[i].confirmed)
{
if(keep < i)
pendingOrders[keep] = pendingOrders[i];
keep++;
}
}
ArrayResize(pendingOrders, keep);
}
//+------------------------------------------------------------------+
`

无声的边界:经纪商讨厌这个



这是课本上永远不会提到的一点。运营B-book(即与客户对赌)的经纪商会主动监控延迟套利。他们的风控系统会标记那些持续从执行速度差异中获利的账户。我见过有账户在部署这个策略几天内就被限制了。

解决方案?随机化。我不会在每一个套利机会上都出手,而是加入50-200毫秒的随机延迟,并且只捕捉60%的信号。这让交易模式看起来像一个普通的剥头皮策略,而不是一个套利机器人。这是一个我在任何地方都没见过有人记录的实用调整。

回测的限制



现实检验就在这里。MT4的策略测试器无法正确模拟
OrderSendAsyncOnTradeTransaction()回调在测试器中的工作方式与实盘不同。对于回测,你必须切换到同步模式,并调整你的预期。这就是一个典型的“MT4回测准确性”问题的例子,MetaQuotes虽然承认这一点,但很少详细讨论。

我参考了CME集团在2018年发布的报告《高频交易:实践指南》,其中证实了这些策略的理论优势是真实存在的,但需要低于50毫秒的执行才能持续。对于零售交易者而言,优势并非来自击败机构基础设施,而是来自利用经纪商自身执行堆栈中的低效。

最后的话



OrderSendAsync`并不适合所有人。它增加了复杂性,产生了跟踪开销,并让你暴露在更多的故障点上。但对于那些依赖时机的策略,它就是盈利优势和亏损系统之间的分水岭。MQL4中异步执行的美妙之处在于,它迫使你将EA视为一个状态机,而不是一个线性脚本。一旦你完成了这种思维转变,一个全新的策略设计世界就会打开。

参考来源



MetaQuotes Software Corp. "OrderSendAsync" 和 "OnTradeTransaction" 文档. docs.mql4.com.
CME Group. "High-Frequency Trading: A Practical Guide". 2018.

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