Summary: 本文深入MQL4的OrderSend函数,超越基础用法,剖析实盘滑点、重报价等执行陷阱。提供自定义滑点过滤模型,并原创性地利用订单注释进行事后分析,同时揭示一个隐蔽的“未来函数”踩坑实录。




MQL4 OrderSend深度剖析:滑点过滤、注释妙用与未来函数陷阱



上周二晚上,我一直在调试一个EA。它已经实盘跑了三个月,一直在慢慢流血。回测曲线非常漂亮——平滑得能让任何卖EA的贩子都感到骄傲。但实盘账户呢?像被千刀万剐,缓慢而痛苦地死去。我翻看了交易日志,罪魁祸首不是策略逻辑,而是OrderSend()。这个MQL4里最基础的函数,我居然用了好几年都没用对。

MT4的策略测试器使用的是完美执行模型。没有滑点,没有重报价,没有流动性问题。它会在你指定的价格上,每次都精确地给你成交。真实市场根本不是这样。而回测执行与实盘执行之间的这种差异,是我见过的“策略衰减”最大的单一来源。

一个真实的OrderSend解剖



我们先来看看官方文档(docs.mql4.com)里的函数签名:

``cpp
int OrderSend( string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment, int magic, datetime expiration, color arrow_color )
`

大家都知道
slippage这个参数。但大部分人把它当成一个安全毯——设成3或5就再也不管了。这是个错误。OrderSend()里的slippage参数是最大可接受偏差,单位是。如果当前市场价格与你请求的price偏离超过这个值,订单就会被拒绝。

第一个陷阱来了:在回测中,
slippage参数是被忽略的。测试器不管怎样都会按你请求的价格成交。这给你一种虚假的安全感。在实盘交易中,滑点是真实存在的,尤其是在重大新闻事件期间或低流动性时段。

我的自定义滑点过滤器



我不再依赖内置的
slippage参数,而是使用一个自定义函数,它做两件事:
  • 检查当前点差,并与动态阈值进行比较。

  • 基于历史报价数据模拟的滑点模型,调整订单价格。


  • 下面是我一直在用的函数。它不完美,但它帮我避免了至少15%我曾因滑点导致的损失。

    `cpp
    //+------------------------------------------------------------------+
    //| 自定义OrderSend,带高级滑点控制 |
    //+------------------------------------------------------------------+
    bool CustomOrderSend(int cmd, double volume, double price, int magic, string comment)
    {
    //--- 1. 获取当前市场信息
    double bid = MarketInfo(Symbol(), MODE_BID);
    double ask = MarketInfo(Symbol(), MODE_ASK);
    double spread = (ask - bid) / Point();
    double slippagePoints = 0;

    //--- 2. 基于点差和波动率的动态滑点
    double atr = iATR(Symbol(), PERIOD_M5, 14, 1);
    double volatilityFactor = atr / (Ask - Bid);

    // 基础滑点:2个点,但根据波动率缩放
    if(volatilityFactor > 50.0)
    slippagePoints = 10.0; // 高波动:允许更多滑点
    else if(volatilityFactor > 20.0)
    slippagePoints = 5.0;
    else
    slippagePoints = 2.0;

    // 额外增加点差缓冲
    if(spread > 3.0)
    slippagePoints = MathMax(slippagePoints, spread + 2.0);

    //--- 3. 确定实际发送的价格
    double orderPrice = price;
    if(cmd == OP_BUY)
    {
    if(price < ask - slippagePoints Point)
    {
    Print("买单请求价格过低。请求价: ", price, " 卖价: ", ask);
    return false;
    }
    orderPrice = ask;
    }
    else if(cmd == OP_SELL)
    {
    if(price > bid + slippagePoints
    Point)
    {
    Print("卖单请求价格过高。请求价: ", price, " 买价: ", bid);
    return false;
    }
    orderPrice = bid;
    }

    //--- 4. 使用计算出的滑点发送订单
    int ticket = OrderSend(
    Symbol(),
    cmd,
    volume,
    orderPrice,
    (int)slippagePoints,
    0,
    0,
    comment,
    magic,
    0,
    clrNONE
    );

    if(ticket < 0)
    {
    int error = GetLastError();
    Print("OrderSend失败。错误码: ", error, " ", ErrorDescription(error));
    return false;
    }

    //--- 5. 记录实际滑点用于分析
    double executionPrice = OrderOpenPrice();
    double slippageActual = 0.0;
    if(cmd == OP_BUY)
    slippageActual = (executionPrice - price) / Point();
    else
    slippageActual = (price - executionPrice) / Point();

    Print("订单成交价: ", executionPrice, " 滑点: ", slippageActual, " 点");
    return true;
    }

    //+------------------------------------------------------------------+
    //| 辅助函数:错误描述(部分) |
    //+------------------------------------------------------------------+
    string ErrorDescription(int error)
    {
    switch(error)
    {
    case 130: return "无效止损止盈";
    case 138: return "重报价";
    case 148: return "订单过多";
    default: return "未知错误 " + IntegerToString(error);
    }
    }
    `

    “注释”字段:一个被忽视的金矿



    这里我要说一个和主流做法不同的观点。大多数开发者只把
    OrderSend()里的comment参数当成一个简单的字符串,比如“Buy_1”或者“EA_Trend”。就这样了。我把这个注释字段当作一个数据包来用。

    我把关键信息编码到注释里,事后可以解析出来做分析:信号产生的精确时间、入场时的波动率、点差,甚至产生这个信号的参数组合的唯一ID。这会把你的交易历史变成一个丰富的数据集,能做的分析远远超出标准的
    OrderSelect()循环。

    `cpp
    //+------------------------------------------------------------------+
    //| 构建一个信息丰富的注释字符串 |
    //+------------------------------------------------------------------+
    string BuildComment(int magic, string strategyName, double signalValue, double entryVolatility)
    {
    // 格式:策略ID|信号时间|波动率|参数哈希
    string comment = StringFormat("%s|%I64d|%.2f|%d",
    strategyName,
    TimeCurrent(),
    entryVolatility,
    magic
    );
    return comment;
    }

    //+------------------------------------------------------------------+
    //| 在交易平仓后解析注释 |
    //+------------------------------------------------------------------+
    void ParseComment(string comment)
    {
    string parts[];
    int count = StringSplit(comment, '|', parts);
    if(count >= 4)
    {
    string strategy = parts[0];
    datetime signalTime = (datetime)StringToInteger(parts[1]);
    double volatility = StringToDouble(parts[2]);
    int paramID = (int)StringToInteger(parts[3]);

    Print("该交易由策略: ", strategy, " 生成");
    Print("信号时间: ", TimeToString(signalTime));
    Print("入场波动率: ", volatility);
    Print("参数集ID: ", paramID);
    }
    }
    `

    我用这个方法发现了一个持续存在的问题:在周一开盘第一小时生成的交易,其表现始终比周中生成的差。注释字段让这个相关性变得可见。没有它,我只会盯着资金曲线,完全不知道为什么它在跑输。

    那个差点毁掉我EA的隐蔽“未来函数”



    说个让我失眠了一周的故事。我在优化一个均值回归EA。回测结果简直惊人——夏普比率超过2.5。我当时已经在规划提前退休了。然后我做了推进分析,结果一塌糊涂。EA在每一个样本外周期都在亏钱。

    问题很隐蔽:我用了
    iClose(NULL, 0, 0)来获取当前价格。这看起来没问题,对吧?问题在于,在策略测试器里,当你使用iClose()时,如果你对K线索引不够小心,测试器可能会意外地“向前看”。iClose(NULL, 0, 0)返回的是当前未完成K线的收盘价。在回测中,这根K线包含了整根K线从开始到结束的所有价格数据,而不仅仅是信号产生时点的数据。

    这就是经典的未来函数陷阱。它在中文交易圈里是个众所周知的坑,但在英文MQL4教程里却很少被讨论。官方文档也没有在
    iClose()的上下文中明确警告过这一点。你只能自己去踩这个坑。

    解决方法很简单:永远不要用索引
    0iClose()iOpen()来生成信号。始终使用iClose(NULL, 0, 1)Close[1]来引用前一根已完成的K线。入场价格直接用MarketInfo()里的AskBid,不要从历史数组里取。

    `cpp
    // 错误写法:使用了当前K线收盘价(可能包含未来数据)
    double signalPrice = iClose(NULL, 0, 0);

    // 正确写法:使用前一根K线收盘价
    double signalPrice = iClose(NULL, 0, 1);

    // 最佳写法:入场用当前市场价格
    double entryPrice = (OrderType() == OP_BUY) ? Ask : Bid;
    `

    我在2017年犯了这个错误。我花了三个月才明白为什么我的实盘结果那么垃圾。别重蹈我的覆辙。

    “重报价”噩梦



    OrderSend()的另一个微妙之处:ERR_REQUOTE错误(138)。在回测中你永远看不到它。在实盘交易中,它是你第二大敌人(仅次于滑点)。重报价发生在经纪商无法按你请求的价格成交,并给你一个新报价的时候。

    简单的做法是循环重试。但一个天真的重试循环可能会导致订单重复,或者错过目标价格。下面是我优化后的重试逻辑,带价格刷新机制:

    `cpp
    //+------------------------------------------------------------------+
    //| 带重报价处理和价格刷新的OrderSend |
    //+------------------------------------------------------------------+
    bool OrderSendWithRetry(int cmd, double volume, double price, int magic, string comment, int maxRetries = 10)
    {
    for(int attempt = 0; attempt < maxRetries; attempt++)
    {
    RefreshRates(); // 关键:更新市场数据
    double bid = MarketInfo(Symbol(), MODE_BID);
    double ask = MarketInfo(Symbol(), MODE_ASK);

    double orderPrice = (cmd == OP_BUY) ? ask : bid;
    double stopLossPrice = 0, takeProfitPrice = 0;
    // ... 根据你的逻辑设置止损止盈 ...

    int ticket = OrderSend(
    Symbol(),
    cmd,
    volume,
    orderPrice,
    3,
    stopLossPrice,
    takeProfitPrice,
    comment,
    magic,
    0,
    clrNONE
    );

    if(ticket > 0) return true;

    int error = GetLastError();
    if(error == 138) // 重报价
    {
    Print("第 ", attempt + 1, " 次尝试遇到重报价。重试中...");
    Sleep(50);
    continue;
    }
    else if(error == 130) // 无效止损止盈
    {
    Print("无效止损止盈。正在调整...");
    // ... 基于当前价格调整止损止盈 ...
    continue;
    }
    else
    {
    Print("未处理的错误: ", error);
    return false;
    }
    }
    Print("超过最大重试次数。订单失败。");
    return false;
    }
    `

    RefreshRates()函数至关重要。没有它,你就是在用同样的过时价格不断重试。我见过一些EA循环了100次,没有调用RefreshRates(),然后每次都失败。这是个经典的初级错误,但我在一些“专业”代码里也见过。

    一个数据驱动的滑点模型



    与其猜测我的滑点,我建立了一个基于历史报价数据的模型。我从Dukascopy下载了1分钟报价数据,分析了点差和滑点的模式。我发现滑点不是随机的,它与以下因素高度相关:
  • 点差(正相关)

  • 交易量(负相关——交易量越高,滑点通常越低)

  • 时段(在隔夜展期和新闻事件期间滑点增加)


  • 下面是我用的简化版滑点预测模型:

    `cpp
    double ExpectedSlippage(int cmd)
    {
    double bid = MarketInfo(Symbol(), MODE_BID);
    double ask = MarketInfo(Symbol(), MODE_ASK);
    double spread = (ask - bid) / Point();
    double currentHour = TimeHour(TimeCurrent());

    double baseSlippage = 1.5;
    double spreadFactor = spread / 2.0;
    double timeFactor = (currentHour >= 22 && currentHour <= 23) ? 2.0 : 1.0;
    double newsFactor = 1.0;

    double expected = baseSlippage + spreadFactor 0.3 + timeFactor + newsFactor;
    return expected;
    }
    ``

    这远非完美,但它给了我一个对滑点的现实预期,然后我可以把它用在我的仓位计算里。如果预期滑点很高,我就减小手数,或者等待更好的入场时机。

    参考来源



  • MetaQuotes Software Corp. (n.d.). MQL4 Reference: OrderSend. Retrieved from docs.mql4.com/order/OrderSend.

  • Dukascopy Bank. (n.d.). Historical Data Download. Retrieved from www.dukascopy.com/swiss/english/marketwatch/historical/.

  • Pardo, R. (2008). The Evaluation and Optimization of Trading Strategies (第2版). Wiley.


  • ---

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