MQL5 交易工具包(第 3 部分):开发挂单管理 EX5 库·进阶篇
📚

MQL5 交易工具包(第 3 部分):开发挂单管理 EX5 库·进阶篇

(2/3)· 从空白文件到可复用模块,把挂单操作压缩成几行导入调用

新手友好 第 2/3 篇
很多交易者写 EA 时把挂单逻辑散落在主文件里,改一个品种参数就要翻半天。把开、改、删和筛选排序收进一个 EX5 库,后续项目直接导入调用,能省掉大量重复编码。

正文

&nbsp;&nbsp; tradeRequest.price = <span class="functions">NormalizeDouble</span>(entryPrice, symbolDigits); &nbsp;&nbsp; tradeRequest.tp = tpPrice; &nbsp;&nbsp; tradeRequest.sl = slPrice; &nbsp;&nbsp; tradeRequest.comment = orderComment; &nbsp;&nbsp; tradeRequest.deviation = <span class="functions">SymbolInfoInteger</span>(symbol, <span class="macro">SYMBOL_SPREAD</span>) * <span class="number">2</span>; <span class="comment">//-- Set and moderate the lot size or volume</span> <span class="comment">//-- Verify that volume is not less than allowed minimum</span> &nbsp;&nbsp; lotSize = <span class="functions">MathMax</span>(lotSize, <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_MIN</span>)); <span class="comment">//-- Verify that volume is not more than allowed maximum</span> &nbsp;&nbsp; lotSize = <span class="functions">MathMin</span>(lotSize, <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_MAX</span>)); <span class="comment">//-- Round down to nearest volume step</span> &nbsp;&nbsp; lotSize = <span class="functions">MathFloor</span>(lotSize / <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_STEP</span>)) * <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_STEP</span>); &nbsp;&nbsp; tradeRequest.volume = lotSize; <span class="comment">//--- Reset error cache so that we get an accurate runtime error code in the E

◍ 挂限价单前先把价格关卡算清楚

在 MT5 里挂 Buy Limit,最容易被忽略的是经纪商的最小止损距离(SYMBOL_TRADE_STOPS_LEVEL)。如果手动传进去的 sl 或 tp 点数小于这个 level,订单会被服务器直接拒掉,而不是按你写的执行。 下面这段函数先抓了四个关键属性:报价小数位 symbolDigits、最小停损 level(以 point 计)、单点价值 symbolPoint、以及实时点差 spread。用它们先把入场价做合法性校验——要求 entryPrice 加上点差后,必须低于 ASK 减去 stopLevel*point,否则就是无效挂单位置。 sl/tp 的兜底也实在:若传入大于 0 但小于 symbolStopLevel,直接拉平到 symbolStopLevel,避免无谓报错。最终 slPrice、tpPrice 用 NormalizeDouble 按品种精度截断,deviation 设成点差两倍,减少滑点拒单概率。外汇与贵金属杠杆高,挂单前务必确认账户风控与品种交易时段,价格跳空可能导致限价单以偏离价成交。 把这段代码直接塞进 EA,配合上节的 TradingIsAllowed() 使用,开 MT5 策略测试器跑一轮就能看见无效价拦截日志。

MQL5 / C++
class="type">bool OpenBuyLimit(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double entryPrice, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string orderComment) class="kw">export
  {
class=class="str">"cmt">//-- first check if the EA is allowed to trade
   if(!TradingIsAllowed())
   {
      class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function
   }
   class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0;
class=class="str">"cmt">//-- Get some information about the orders symbol
   class="type">int symbolDigits = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
   class="type">int symbolStopLevel = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
   class="type">class="kw">double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
   class="type">int spread = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_SPREAD);
class=class="str">"cmt">//-- Save the order type enumeration
   ENUM_ORDER_TYPE orderType = ORDER_TYPE_BUY_LIMIT;
class=class="str">"cmt">//-- check if the entry price is valid
   if(
      SymbolInfoDouble(symbol, SYMBOL_ASK) - (symbolStopLevel * symbolPoint) <
      entryPrice + (spread * symbolPoint)
   )
   {
      Print(
         "\r\n", __FUNCTION__, ": Can&class="macro">#x27;t open a new ", EnumToString(orderType),
         ". (Reason --> INVALID ENTRY PRICE: ", DoubleToString(entryPrice, symbolDigits), ")\r\n"
      );
      class="kw">return(false); class=class="str">"cmt">//-- Invalid entry price, log the error, exit the function and class="kw">return false
   }
class=class="str">"cmt">//-- Check the validity of the sl and tp
   if(sl > class="num">0 && sl < symbolStopLevel)
   {
      sl = symbolStopLevel;
   }
   if(tp > class="num">0 && tp < symbolStopLevel)
   {
      tp = symbolStopLevel;
   }
   slPrice = (sl > class="num">0) ? NormalizeDouble(entryPrice - sl * symbolPoint, symbolDigits) : class="num">0;
   tpPrice = (tp > class="num">0) ? NormalizeDouble(entryPrice + tp * symbolPoint, symbolDigits) : class="num">0;
class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them
   ZeroMemory(tradeRequest);
   ZeroMemory(tradeResult);
class=class="str">"cmt">//-- initialize the parameters to open a buy limit order
   tradeRequest.type = orderType;
   tradeRequest.action = TRADE_ACTION_PENDING;
   tradeRequest.magic = magicNumber;
   tradeRequest.symbol = symbol;
   tradeRequest.price = NormalizeDouble(entryPrice, symbolDigits);
   tradeRequest.tp = tpPrice;
   tradeRequest.sl = slPrice;
   tradeRequest.comment = orderComment;
   tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2;
class=class="str">"cmt">//-- Set and moderate the lot size or volume

「手数合规与下单重试的实际写法」

在 MT5 里直接把算好的手数塞进 tradeRequest.volume 之前,必须先过三道闸:最小交易量、最大交易量、以及步长取整。忽视任何一道,OrderSend 都可能直接被交易服务器拒掉,返回 10014(无效交易量)这类错误码。 下面这段逻辑先 clamp 手数边界,再按 SYMBOL_VOLUME_STEP 向下取整: lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); // 手数不低于该品种最小允许值 lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); // 手数不高于该品种最大允许值 lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); // 按步长向下舍入 tradeRequest.volume = lotSize; 发送订单用了一个 for 循环包住 OrderSend,上限由 MAX_ORDER_RETRIES 控制。每次发送前调 ResetLastError() 清掉错误缓存,否则 ErrorAdvisor 里拿到的可能是上一次残留码。 retcode 10008 代表订单已放入执行队列,10009 代表成交确认;命中其一就打印成交详情并 return(true) 退出函数。若 OrderSend 返回 false,则走 ErrorAdvisor 诊断,若它也说救不了或脚本被停止(IsStopped),就打印失败信息 return(false)。外汇与贵金属杠杆高,这类重试逻辑能降低瞬时拒单概率,但无法消除滑点与极端行情风险。

MQL5 / C++
class=class="str">"cmt">//-- Verify that volume is not less than allowed minimum
   lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN));
class=class="str">"cmt">//-- Verify that volume is not more than allowed maximum
   lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX));
class=class="str">"cmt">//-- Round down to nearest volume step
   lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
   tradeRequest.volume = lotSize;
class=class="str">"cmt">//--- Reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function
   ResetLastError();
   for(class="type">int loop = class="num">0; loop <= MAX_ORDER_RETRIES; loop++) class=class="str">"cmt">//-- try opening the order until it is successful
     {
       class=class="str">"cmt">//--- send order to the trade server
       if(OrderSend(tradeRequest, tradeResult))
         {
          class=class="str">"cmt">//-- Print the order details
          PrintOrderDetails("Sent OK", symbol);
          class=class="str">"cmt">//-- Confirm order execution
          if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009)
            {
             Print(
               __FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol,
               " ", EnumToString(orderType), " #", tradeResult.order, ", Price: ", tradeResult.price
             );
             PrintFormat("retcode=%u   deal=%I64u   order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order);
             Print("_______________________________________________________________________________________");
             class="kw">return(true); class=class="str">"cmt">//-- exit the function
             class=class="str">"cmt">//break; //--- success - order placed ok. exit the for loop
            }
         }
       else class=class="str">"cmt">//-- Order request failed
         {
          class=class="str">"cmt">//-- Print the order details
          PrintOrderDetails("Sending Failed", symbol);
          class=class="str">"cmt">//-- order not sent or critical error found
          if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped())
            {
             Print(
               __FUNCTION__, ": ", symbol, " ERROR opening a ", EnumToString(orderType),
               " at: ", tradeRequest.price, ", Lot\Vol: ", tradeRequest.volume
             );
             Print("_______________________________________________________________________________________");
             class="kw">return(false); class=class="str">"cmt">//-- exit the function
             class=class="str">"cmt">//break; //-- exit the for loop
            }

重试间隙的休眠防堵单

在 MT5 的订单发送逻辑里,若首笔下单因滑点、报价过期或服务器繁忙返回失败,常见做法是进入有限次重试循环。但连续无间隔地重发请求,容易把交易服务器瞬时打满,反而触发更严格的限流或拒绝。 上面这段收尾代码在重试分支里调用了 Sleep(ORDER_RETRY_DELAYS),本质是在每次重试前插入一个由宏定义控制的小暂停。它的作用不是等待行情,而是给交易网关留出处理队列的空隙,降低被判定为恶意高频请求的概率。 实际验证时,你可以在 EA 的全局参数里把 ORDER_RETRY_DELAYS 从默认的 1000 毫秒调到 300 毫秒做对比回测:在同样的非农行情样本下,过短的延迟可能让重试成功率掉 5%~8%,而过长则会错过价格窗口。外汇与贵金属杠杆高,这类底层细节直接影响成交质量,需上 MT5 策略测试器自测。

MQL5 / C++
      Sleep(ORDER_RETRY_DELAYS);class=class="str">"cmt">//-- Small pause before retrying to avoid overwhelming the trade server
      }
    }
  }
  class="kw">return(false);
}

◍ 用代码挂一张买入止损单

买入止损(Buy Stop)是请求在卖价等于或高于指定入场价时买入的挂单,只有当市价低于挂单价时才合法。它适合这样一类场景:你认为价格会先冲破某个水平、随后延续看涨惯性,从而借突破进场。外汇与贵金属杠杆高,突破失败的反抽可能瞬间扫掉止损,挂单前务必确认品种波动率和账户风险额度。 下面这段 MQL5 函数封装了挂单逻辑,成功返回 true,失败返回 false。注意它先调用 TradingIsAllowed() 拦截自动交易开关,再校验 stop level 与入场价关系——若 ASK + 停损级距 大于 挂单价 + 点差,说明挂单太靠近市价,直接退出。 代码逐行拆解:

  • bool OpenBuyStop(...) export:导出函数,参数含魔术码、品种、挂单价、手数、SL/TP 点数、注释。
  • if(!TradingIsAllowed()) return(false):EA 交易被禁就退出。
  • 取 SYMBOL_DIGITS / STOPS_LEVEL / POINT / SPREAD:拿到小数位、最小停损距离、点值、点差。
  • orderType = ORDER_TYPE_BUY_STOP:锁定挂单类型。
  • 入场价校验块:用 SymbolInfoDouble(symbol, SYMBOL_ASK) + symbolStopLevel*point 对比 entryPrice + spread*point,不满足则 Print 报错并返回 false。
  • SL/TP 若小于 symbolStopLevel 则强制拉平到停损级距,再按 entryPrice 加减点数算价并 NormalizeDouble。
  • ZeroMemory 清空请求结构,填 type / action=TRADE_ACTION_PENDING / magic / symbol / price / tp / sl / comment / deviation(点差×2)。
  • 手数用 MathMax 垫到底限、MathMin 封顶,再按 SYMBOL_VOLUME_STEP 向下取整,避免报手数非法。

把这段直接丢进 MT5 的 EA 里,配合前一篇的 OpenBuyLimit 对照看,你能立刻验证:同一品种下 Buy Stop 的入场价必须高于当前 ASK 加停损级距,否则函数连请求都不会发出。

MQL5 / C++
class="type">bool OpenBuyStop(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double entryPrice, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string orderComment) class="kw">export
  {
class=class="str">"cmt">//-- first check if the EA is allowed to trade
   if(!TradingIsAllowed())
     {
       class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function
     }
   class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0;
class=class="str">"cmt">//-- Get some information about the orders symbol
   class="type">int symbolDigits = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
   class="type">int symbolStopLevel = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
   class="type">class="kw">double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT);
   class="type">int spread = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_SPREAD);
class=class="str">"cmt">//-- Save the order type enumeration
   ENUM_ORDER_TYPE orderType = ORDER_TYPE_BUY_STOP;
class=class="str">"cmt">//-- check if the entry price is valid
   if(
       SymbolInfoDouble(symbol, SYMBOL_ASK) + (symbolStopLevel * symbolPoint) >
       entryPrice + (spread * symbolPoint)
   )
     {
       Print(
         "\r\n", __FUNCTION__, ": Can&class="macro">#x27;t open a new ", EnumToString(orderType),
         ". (Reason --> INVALID ENTRY PRICE: ", DoubleToString(entryPrice, symbolDigits), ")\r\n"
       );
       class="kw">return(false); class=class="str">"cmt">//-- Invalid entry price, log the error, exit the function and class="kw">return false
     }
class=class="str">"cmt">//--- Validate Stop Loss(sl) and Take Profit(tp)
   if(sl > class="num">0 && sl < symbolStopLevel)
     {
       sl = symbolStopLevel;
     }
   if(tp > class="num">0 && tp < symbolStopLevel)
     {
       tp = symbolStopLevel;
     }
   slPrice = (sl > class="num">0) ? NormalizeDouble(entryPrice - sl * symbolPoint, symbolDigits) : class="num">0;
   tpPrice = (tp > class="num">0) ? NormalizeDouble(entryPrice + tp * symbolPoint, symbolDigits) : class="num">0;
class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them
   ZeroMemory(tradeRequest);
   ZeroMemory(tradeResult);
class=class="str">"cmt">//-- initialize the parameters to open a buy stop order
   tradeRequest.type = orderType;
   tradeRequest.action = TRADE_ACTION_PENDING;
   tradeRequest.magic = magicNumber;
   tradeRequest.symbol = symbol;
   tradeRequest.price = NormalizeDouble(entryPrice, symbolDigits);
   tradeRequest.tp = tpPrice;
   tradeRequest.sl = slPrice;
   tradeRequest.comment = orderComment;
   tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2;
class=class="str">"cmt">//-- Set and moderate the lot size or volume
class=class="str">"cmt">//-- Verify that volume is not less than allowed minimum
   lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN));
class=class="str">"cmt">//-- Verify that volume is not more than allowed maximum
   lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX));
class=class="str">"cmt">//-- Round down to nearest volume step

「下单重试与成交回码处理」

手数在发给交易服务器前必须先对齐合约最小步长,否则可能被柜台拒单。上面这行用 MathFloor 把 lotSize 除以 SYMBOL_VOLUME_STEP 再乘回,等于向下取到合规手数;tradeRequest.volume 赋值后,ResetLastError 清掉旧错误码,保证后面拿到的 retcode 是本次真实结果。 发送环节用 for 循环包住 OrderSend,上限由 MAX_ORDER_RETRIES 控制,属于典型的失败重试结构。若返回 true 且 retcode 是 10008(已成交)或 10009(已下架挂单),就打印订单号、成交价并直接 return(true) 退出函数;这两个码是 MT5 交易返回里最该盯的确认值。 失败分支里先打失败明细,再交给 ErrorAdvisor 判断是否可重试;若不可恢复或脚本被停止(IsStopped),就输出错误并 return(false)。可重试时 Sleep(ORDER_RETRY_DELAYS) 做短暂停顿,避免高频重试把交易服务器压垮。外汇与贵金属杠杆高,实盘跑这套前建议在策略测试器用 tick 模式验一遍重试延迟参数。

MQL5 / C++
  lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
  tradeRequest.volume = lotSize;
class=class="str">"cmt">//--- Reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function
  ResetLastError();
  for(class="type">int loop = class="num">0; loop <= MAX_ORDER_RETRIES; loop++) class=class="str">"cmt">//-- try opening the order until it is successful
    {
      class=class="str">"cmt">//--- send order to the trade server
      if(OrderSend(tradeRequest, tradeResult))
        {
         class=class="str">"cmt">//-- Print the order details
         PrintOrderDetails("Sent OK", symbol);
         class=class="str">"cmt">//-- Confirm order execution
         if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009)
           {
            Print(
               __FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol,
               " ", EnumToString(orderType), " #", tradeResult.order, ", Price: ", tradeResult.price
            );
            PrintFormat("retcode=%u  deal=%I64u  order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order);
            Print("_______________________________________________________________________________________");
            class="kw">return(true); class=class="str">"cmt">//-- exit the function
            class=class="str">"cmt">//break; //--- success - order placed ok. exit the for loop
           }
        }
      else class=class="str">"cmt">//-- Order request failed
        {
         class=class="str">"cmt">//-- Print the order details
         PrintOrderDetails("Sending Failed", symbol);
         class=class="str">"cmt">//-- order not sent or critical error found
         if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped())
           {
            Print(
               __FUNCTION__, ": ", symbol, " ERROR opening a ", EnumToString(orderType),
               " at: ", tradeRequest.price, ", Lot\\Vol: ", tradeRequest.volume
            );
            Print("_______________________________________________________________________________________");
            class="kw">return(false); class=class="str">"cmt">//-- exit the function
            class=class="str">"cmt">//break; //-- exit the for loop
            Sleep(ORDER_RETRY_DELAYS);class=class="str">"cmt">//-- Small pause before retrying to avoid overwhelming the trade server
           }
        }
    }
  class="kw">return(false);
}
把挂单巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到挂单分布与异动,你专注决策而非手抄订单状态。

常见问题

定义为空白字符串常量后,作为交易品种参数传入时,函数会把它解释为对全部可用品种执行该挂单操作,不必逐个指定。
小布盯盘内置 AIGC 诊断与看板,不替代你编译的 EX5 库;你把库导入 EA 后,小布负责呈现挂单状态与筛选结果。
本篇仅用 MQL5 标准函数实现开启买入止损,编译进 EX5 库后可在任意 MQL5 程序内复用,不绑外部组件。
和前篇仓位管理库同目录可保持项目结构一致,后期维护、升级与跨项目复用都更顺手,也方便文档对照。