Summary: 本文探讨了MQL4中MarketInfo()在稳健交易执行方面被忽视的强大功能。它提供了用于预验证订单、处理经纪商特定保证金规则的实用代码,并就如何解读OrderSend错误提供了独特视角。




我们来聊聊 OrderSend()。每个MQL4开发者都认识这个函数。你把参数喂给它,它发出指令,然后你得到一个订单号。或者一个错误码。然后呢?我们大多数人只是打印出错误,也许做个Sleep(1000)然后重试。这是条轻松的路。

我曾经花了一年时间为一家自营交易公司开发EA,这家公司有一些……怎么说呢 另类 的执行规则。他们的点差是浮动的,保证金要求会根据账户净值变化,每笔交易的最大手数也是个移动靶。我那套简单的重试逻辑EA被虐得体无完肤。就在那时我意识到:OrderSend()只是个扳机。执行的艺术真正在于预验证和理解你发送订单所处的环境。

而实现这一环境分析的秘密武器就是 MarketInfo()。官方的MQL4文档(docs.mql4.com)列出了它的模式,但它几乎没有触及如何将这些调用串联起来以构建订单执行的预测模型。这不是什么光鲜亮丽的工作,但它决定了EA是生存下来还是爆仓。

超越MODE_SPREAD:被低估的MarketInfo查询



我们都知道 MarketInfo(Symbol(), MODE_SPREAD)。但让我们深入挖掘那些阴暗的角落。

MODE_MINLOTMODE_MAXLOT:显而易见,对吧?但 MODE_LOTSTEP 呢?我数不清有多少EA试图在经纪商手数步长为0.1的情况下发送0.07手。OrderSend() 失败并返回错误131(无效手数)。一个简单的预检查本可以避免这个问题。但等等,还有更多。如果你计算出的手数是0.15,而步长是0.1呢?你需要向下舍入到0.1,或者舍入到最近的步长。这很基础,但我看到它不断被忽视。

MODE_STOPLEVEL:这是经纪商强制规定的止损和止盈最小距离,通常以点为单位。如果你试图在一个止盈止损最小距离为20点的经纪商上设置10点的止损,你的订单将被拒绝并返回错误130(无效止损)。在调用 OrderSend() 之前 知道这个值是至关重要的。

MODE_FREEZELEVEL:这是一个奇怪的东西。它返回冻结水平,即挂单不能被修改或删除的距离(以点为单位)。如果你试图修改一个在冻结水平内的订单,你会得到一个错误。了解这一点有助于你判断修改是否值得尝试。

隐藏的怪癖:MODE_MARGINREQUIRED 和净值陷阱



这里开始变得有趣了。这也是我自营交易公司经验的用武之地。

MarketInfo(Symbol(), MODE_MARGINREQUIRED) 返回开一手仓位所需的保证金。文档对此说得很清楚。隐藏的点,也是让我栽跟头的点,是这个值会根据你账户的杠杆(这是静态的)和品种的保证金率(这可以是动态的)而变化。一些经纪商在特定时段为主要货币对提供降低的保证金率,或在新闻事件前提高保证金率。

这个怪癖在于:MarketInfo(Symbol(), MODE_MARGINREQUIRED) 给你的是
每手所需的保证金。但如果你的账户净值接近强行平仓水平呢?你可以计算你能开的最大手数:

最大手数 = (AccountFreeMargin() - 缓冲保证金) / MarketInfo(Symbol(), MODE_MARGINREQUIRED)

“缓冲保证金”是我自己加的。这是一个安全网。我总是额外加上计算保证金的20-30%作为缓冲,以应对可能导致订单刚发出就触发自动强行平仓的价格滑点或突然的点差扩大。

AccountFreeMargin() 检查是一个标准做法,但我更进一步。在发送订单之前,我实际上计算了订单开立后的
可用保证金,并检查它是否高于某个阈值。这可以防止你的EA开了一笔交易,然后下一个Tick因为可用保证金刚好低于经纪商阈值而触发保证金强平的糟糕情况。

我的“订单起飞前检查清单”函数



我构建了一个名为 TradePreFlightCheck() 的函数来封装所有这些检查。它有点长,但让我免于无数次执行错误。

``mql4
//+------------------------------------------------------------------+
//| 交易起飞前检查 |
//+------------------------------------------------------------------+
bool TradePreFlightCheck(string symbol, double lotSize, double slPoints, double tpPoints, double &outLots)
{
// 重置输出手数
outLots = lotSize;

// 1. 检查是否允许交易
if(!IsTradeAllowed())
{
Print("起飞前检查错误: 交易上下文忙碌或不允许交易。");
return false;
}

// 2. 验证手数
double minLot = MarketInfo(symbol, MODE_MINLOT);
double maxLot = MarketInfo(symbol, MODE_MAXLOT);
double lotStep = MarketInfo(symbol, MODE_LOTSTEP);

if(lotStep == 0) {
Print("起飞前检查错误: 手数步长为零。无法计算有效手数。");
return false;
}

// 调整手数到最接近的有效步长,向下取整以避免超过最大手数
double normalizedLots = MathFloor(outLots / lotStep)
lotStep;

// 如果标准化后的手数低于最小值,设为最小值。
if(normalizedLots < minLot) normalizedLots = minLot;
// 如果标准化后的手数高于最大值,设为最大值。
if(normalizedLots > maxLot) normalizedLots = maxLot;

// 如果经过这一切,标准化后的手数仍然不在范围内,这是一个严重错误。
if(normalizedLots < minLot || normalizedLots > maxLot)
{
Print("起飞前检查错误: 手数 ", normalizedLots, " 超出范围。");
return false;
}

outLots = normalizedLots; // 更新输出引用

// 3. 验证止损和止盈水平
int stopLevel = (int)MarketInfo(symbol, MODE_STOPLEVEL);
int freezeLevel = (int)MarketInfo(symbol, MODE_FREEZELEVEL);
int minDist = MathMax(stopLevel, freezeLevel); // 我们必须同时尊重两者

if(slPoints > 0 && slPoints < minDist)
{
Print("起飞前检查错误: 止损 (", slPoints, " 点) 小于最小距离 (", minDist, " 点)。");
return false;
}

if(tpPoints > 0 && tpPoints < minDist)
{
Print("起飞前检查错误: 止盈 (", tpPoints, " 点) 小于最小距离 (", minDist, " 点)。");
return false;
}

// 4. 检查可用保证金
double marginPerLot = MarketInfo(symbol, MODE_MARGINREQUIRED);
if(marginPerLot == 0 || marginPerLot > AccountFreeMargin())
{
Print("起飞前检查错误: 可用保证金不足。每手所需: ", marginPerLot, ", 可用保证金: ", AccountFreeMargin());
return false;
}

// 我的缓冲逻辑:确保交易后至少有30%的额外保证金
double requiredMargin = outLots marginPerLot;
double freeMarginAfterTrade = AccountFreeMargin() - requiredMargin;
double bufferThreshold = AccountFreeMargin()
0.3; // 30% 缓冲

if(freeMarginAfterTrade < bufferThreshold)
{
Print("起飞前检查错误: 保证金缓冲不足。交易后可用保证金: ", freeMarginAfterTrade, ", 所需缓冲: ", bufferThreshold);
return false;
}

// 5. 检查市场是否开盘(可选但推荐)
if(!MarketInfo(symbol, MODE_TRADEALLOWED))
{
Print("起飞前检查错误: 品种 ", symbol, " 不允许交易");
return false;
}

return true; // 所有检查通过
}
//+------------------------------------------------------------------+
`

关于错误处理的另类视角



这是我的独特观点:大多数人将
OrderSend() 错误视为需要重试的失败。我则视其为数据点。当我收到一个错误时,我会记录一切:错误代码、所有输入的确切值,以及那一刻的 MarketInfo()` 值。随着时间的推移,这就创建了一个经纪商行为日志。

例如,我有一个EA不断收到错误128(交易超时)。日志显示它只发生在伦敦开盘时段,那时波动性激增。我没有盲目重试,而是增加了逻辑来检测这种模式,并在该时段增加滑点容忍度。它起作用了。错误128消失了。

MetaQuotes官方关于“OrderSend错误处理”的文章建议了一个简单的重试循环。那是主流方法。我的方法则将错误处理视为一种自适应执行调优,实时地从经纪商环境中学习。

参考来源
  • MetaQuotes Software Corp. MQL4文档. "MarketInfo." docs.mql4.com/marketinformation.

  • MetaQuotes Software Corp. MQL4文档. "OrderSend." docs.mql4.com/trading/ordersend.


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