MT5编程方法

MT5订单执行质量实战:滑点、部分成交、重试与幂等的完整处理

晓
晓辉
🕐 2026年11月12日 👁 1116 阅读
MT5订单执行质量实战:滑点、部分成交、重试与幂等的完整处理
M

风险提示:本文为软件开发与技术服务类技术文章,仅用于MQL5程序化开发的技术交流与软件工程实践探讨。文中涉及的订单发送参数、滑点采样方法、retcode处理策略、重试与幂等设计等内容,均为软件工程原理演示与教学用途,不构成任何投资建议、交易策略推荐或收益预期。外汇、贵金属、CFD等杠杆交易存在较高风险,可能导致重大本金损失。文中代码片段仅供学习参考,需经人工审查、编译测试与充分的模拟盘验证后方可使用,直接投入实盘可能造成资金损失。本网站仅提供软件开发技术服务,不涉及任何交易平台运营、经纪业务或投资咨询业务。

回测曲线漂亮,实盘却总差一截。多数人本能反应是策略失效,但把成交明细导出来逐笔看,问题常常出在下单这一环:成交价和下单时的报价差了几个点,重试逻辑把一笔单发成了两笔,请求1.0手只回来0.3手。这些损耗不会抛异常,也不会写进日志的错误行,只会安静地吃利润。

一、执行损耗的四个来源

不铺垫概念,先把实盘里会真实发生的失败列清楚。下面四类在排查中出现频率较高,注意它们的报错特征并不一样。

失败类型 典型现象 常见 retcode 危害
滑点 成交价比请求价差3至8点 无报错,静默发生 每笔多吃成本,短周期策略直接由盈转亏
拒单 / 重新报价 OrderSend 返回 false 10004 REQUOTE、10006 REJECT 信号丢失,实盘行为与回测不符
部分成交 请求1.0手只成交0.3手 10009 DONE,但 volume 变小 仓位不符合风控假设,敞口算错
重复下单 同一信号开出两笔 无报错 风险翻倍,极端行情下可能触发强平

四类里只有第二类会让接口返回失败,其余三类都是「调用成功、结果不对」。这就是为什么很多 EA 的日志干干净净,账户曲线却在慢性失血——你在等一个报错,而它不会来。

重点提示:判断下单链路是否健康,不能只看 OrderSend 的返回值。滑点、部分成交、重复下单都不会报错,需要单独的采样与对账机制才能发现,这也是本文后面四个环节要解决的问题。

二、滑点不是随机数,是可以统计的

deviation 的语义是「允许的价格偏差上限」,超出就拒单或重新报价。它是个开关,不是测量工具:设成20点不代表滑点就是20点,更不代表没有滑点。只看这个数字调参,等于闭着眼睛做优化。

要知道实际滑了多少,只能自己采样。做法四步:下单前把 MqlTick 的 ask/bid 留一份作为请求价基准;成交后读回报里的成交价;按买卖方向换算成点数,正数表示对你不利;样本累积后算中位数、90分位与超限率。

double g_slip[];   // EA 顶部声明,滑点样本缓冲

double RecordSlippage(const MqlTradeRequest &req, const MqlTradeResult &res)
{
   if(res.retcode != TRADE_RETCODE_DONE && res.retcode != TRADE_RETCODE_PLACED)
      return 0.0;
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   double slip  = (req.type == ORDER_TYPE_BUY)
                  ? (res.price - req.price) / point    // 买:成交价更高为正滑点
                  : (req.price - res.price) / point;   // 卖:成交价更低为正滑点
   int n = ArraySize(g_slip);
   ArrayResize(g_slip, n + 1);
   g_slip[n] = slip;
   return slip;
}

void PrintSlippageStats(double deviationPts)
{
   int n = ArraySize(g_slip);
   if(n == 0) return;
   ArraySort(g_slip);
   int over = 0;
   for(int i = 0; i < n; i++)
      if(g_slip[i] > deviationPts) over++;
   PrintFormat("[滑点] n=%d 中位=%.1f p90=%.1f 超限率=%.1f%%",
               n, g_slip[n / 2], g_slip[(int)(n * 0.9)], over * 100.0 / n);
}

知识点:res.price 是券商回报的成交价,req.price 是你提交的请求价,两者不相等才是滑点的定义。很多人拿图表当前价和成交价比,那是两个不同时点的报价,比出来的数字没有意义。

统计周期建议按周,样本少于30笔时先别下结论。中位数告诉你常态成本,90分位告诉你坏情况有多坏,超限率则直接反映 deviation 设得合不合理——如果超限率长期高于5%,说明你给的偏差空间偏窄,拒单会明显增多,信号丢失率跟着上升。

进阶思考:滑点水平与账户执行模型强相关。ECN/STP 与做市账户的报价来源不同,同一策略在两个账户上跑出的滑点分布不能横向比较。建议按品种、按时段(亚盘、欧盘、美盘,以及重要数据发布前后)分层统计,才能定位到底是哪一段在漏钱,而不是笼统地归因于「券商不行」。

三、retcode 分类:哪些该重试,哪些该放弃

拿到 retcode 之后,要先做的不是重发,是分类。有些错误重发一次就成,有些错误重发只会把亏损放大。下表按 MQL5 官方文档的枚举含义整理,可作为重试策略的判定依据。

retcode 含义 是否可重试 重试前提 建议次数
10004 REQUOTE 重新报价 可 重新读取报价后再发 2
10020 PRICE_CHANGED 价格已变 可 同步最新价 2
10021 NO_PRICES 无报价 可 等报价恢复,拉开间隔 3
10006 REJECT 被拒绝 不可 多为保证金或参数问题 0
10013 INVALID 请求无效 不可 检查手数、止损距离是否违反券商规则 0
10018 MARKET_CLOSED 市场关闭 不可 等下一个交易时段 0
10012 TIMEOUT 请求超时 条件性 须先回查服务器侧是否已成交 见下一章
bool IsRetryable(uint code)
{
   return (code == TRADE_RETCODE_REQUOTE       ||   // 10004 重新报价
           code == TRADE_RETCODE_PRICE_CHANGED ||   // 10020 价格已变
           code == TRADE_RETCODE_NO_PRICES);        // 10021 无报价
   // 10012 超时不在此列,必须回查后再决定,见第4章
}

bool SendWithRetry(MqlTradeRequest &req, MqlTradeResult &res, int maxTry = 2)
{
   MqlTick tick;
   for(int i = 0; i <= maxTry; i++)
   {
      if(OrderSend(req, res) && res.retcode == TRADE_RETCODE_DONE)
         return true;
      if(!IsRetryable(res.retcode) || i == maxTry)
         return false;
      Sleep(300 * (i + 1));                    // 退避,避免连续打券商
      SymbolInfoTick(_Symbol, tick);
      req.price = (req.type == ORDER_TYPE_BUY) ? tick.ask : tick.bid;  // 换最新价
   }
   return false;
}

风险提醒:10012(TRADE_RETCODE_TIMEOUT)是最容易出事的一类。超时不等于失败,订单可能已经在服务器侧成交,只是回报没回来。此时直接重发,结果就是两笔仓位。这类错误不能进通用重试分支,必须先回查再决定。

重点提示:重试要设上限,并且每次重发前重新取价。用上一轮的旧价格重发,等于主动制造一次滑点。一个可用的经验配置是:10004 与 10020 重试2次,10021 重试3次并拉开间隔,其余错误不重试。

还有一种情况要单独处理:请求1.0手,返回10009(DONE),但回报里的 volume 只有0.3。这是部分成交,在流动性偏薄的时段属于正常现象。此时仓位与你的风控假设不符,要么按剩余量补齐,要么接受并在资金管理里按实际持仓重算敞口——但不能默认「发多少就成交多少」。

四、幂等:重试前先确认「到底发出去没有」

所有涉及超时、连接中断、返回值为空的场景,都不能凭返回值判断成败。判断依据只有一个:服务器侧到底有没有这笔单。三个回查手段,成本从低到高:扫活动订单、扫近期成交历史、用请求标识精确匹配。

bool AlreadySent(string sigId, long magic, int windowSec = 120)
{
   // 1) 扫活动订单
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderGetInteger(ORDER_MAGIC) != magic)       continue;
      if(OrderGetString(ORDER_COMMENT) == sigId)      return true;
   }
   // 2) 扫近期成交历史
   datetime from = TimeCurrent() - windowSec;
   if(!HistorySelect(from, TimeCurrent()))
      return true;                 // 取不到历史就当已发:宁可漏单,不重单
   for(int j = HistoryDealsTotal() - 1; j >= 0; j--)
   {
      ulong t = HistoryDealGetTicket(j);
      if(HistoryDealGetString(t, DEAL_COMMENT) == sigId) return true;
   }
   return false;
}

// 调用示例:下单前先落幂等键
string sigId = StringFormat("sig_%s_%d", _Symbol, (int)TimeCurrent());
req.comment = sigId;
if(AlreadySent(sigId, InpMagic)) return;   // 已发过,直接退出

知识点:comment 是券商侧会原样带回的字段,长度有限(通常几十个字符以内),适合放短信号 ID,例如 sig_EURUSD_1730_01。不要放长文本或中文说明,一旦被截断,匹配就会失败,幂等形同虚设。

进阶思考:幂等设计的取舍原则是「宁可漏一笔,不要重一笔」。漏单只损失一次机会,重单是敞口翻倍,在杠杆品种上可能直接触发强平。因此回查失败时的默认动作应当是放弃本次下单并记录告警,而不是继续发。

顺带说一个常见坑:HistorySelect 的时间窗不能开得太窄。券商回报有延迟,用 now-30秒 去查,很可能查不到刚刚那笔,于是判定「没发出去」再发一次。时间窗建议120秒起步,并且查不到历史时按「已发」处理,让代码走保守分支。

五、把执行质量做成每日报表

滑点分布、重试成功率、部分成交率这三项指标,每天收盘后打印一行,连续积累20个交易日,就能回答一个关键问题:这段亏损是策略逻辑的问题,还是执行环节的问题。前者要改模型,后者要改工程,方向完全不同。

[执行质量] 2026-11-12 滑点中位=1.2点 p90=4.8点 超限率=2.1% | 重试3次 成功2次 | 部分成交1笔(30%)

操作参考:落地顺序建议如下——① 本周加滑点采样,先有数据再谈优化;② 本周把 retcode 分类表落成 IsRetryable(),替换掉散落在各处的 if(retry);③ 下周加幂等键,comment 写信号 ID;④ 持续做每日一行报表,20个交易日后回看,再决定要不要调 deviation。

最后说清一件事:执行质量改善不改变策略本身的盈亏属性。一个负期望的策略,把滑点从4点压到1点仍然是负期望,只是亏得慢一些;但对一个正期望的策略,这部分损耗往往正是「回测能赚、实盘不赚」的那条缝。先有数据,再谈优化,顺序反了都是空转。

风险提示:本文内容仅供软件开发与技术学习参考,不构成任何投资建议或交易指导。程序化交易存在较高风险,可能导致重大本金损失。文中涉及的订单发送参数、滑点采样、retcode 处理、重试与幂等设计等代码,均为技术原理演示,未经完整工程化封装,直接复制使用可能存在逻辑缺陷、边界条件遗漏与券商兼容性问题,需经人工审查、编译测试与充分的模拟盘验证。retcode 数值与含义依据 MQL5 官方文档整理,实际行为以对应版本官方文档与实测结果为准。执行质量的改善不改变策略本身的盈亏属性。实盘应用前请自行决策并承担相应风险。本网站仅提供软件开发技术服务,不涉及任何交易平台运营、经纪业务或投资咨询业务。

第三方免责声明:本文仅代表作者个人技术观点,与任何经纪商、交易平台或金融机构无关。文中提及的 MetaTrader 5、MQL5、MQL4、MetaEditor、OrderSend、MqlTradeRequest、MqlTradeResult、MqlTick、TRADE_RETCODE_* 系列常量、HistorySelect、OrderSelect 等名称与产品,其商标与权利归各自所有者所有,本文仅作技术说明与交流引用,不构成任何推荐、背书或商业合作关系。文中关于券商执行模型与回报延迟的描述基于公开资料与工程实践,具体表现以各平台官方说明为准。读者应自行核实相关信息,因使用本文内容而产生的直接或间接损失,作者不承担任何责任。本文不推荐任何具体的经纪商、交易平台或金融产品。

扫码关注,获取更多EA开发干货

晓辉编程微信公众号

微信公众号

晓辉编程视频号

视频号

🎲 猜你喜欢

首页 文章 下载 产品 定制
回到顶部 ↑ 搜索 🔍 EA定制 ⚙️
微信咨询 💬
晓辉微信二维码

扫码加微信
XiaoHuiProgramming