MQL5 Cookbook - 以 MQL5 编写的多币种 EA,利用限价订单工作·进阶篇
⏱️

MQL5 Cookbook - 以 MQL5 编写的多币种 EA,利用限价订单工作·进阶篇

(2/3)· 从模板到日内范式,6 节拆解多品种挂单的布置、反转与事件处理陷阱

含代码示例偏理论 第 2/3 篇
不少人在多币种 EA 里直接沿用连续交易模板,忘了限价单在跨时区会话中会堆积无效挂单。把欧洲和美国时段混成一锅煮,回测漂亮实盘却总在流动性真空被扫损。时段边界不划清,优化参数只是在噪音里找规律。

「挂单属性的结构体与枚举映射」

在 MT5 的 MQL5 环境里,想批量读取挂单特征,先得把订单属性枚举和结构体对齐。下面这段把 O_VOLUME_CURRENT 到 O_ALL 编成 7~15 的整型常量,相当于给挂单属性槽位标了号,后续 switch 分发时直接按号取字段。 真正落地用的是 pending_order_properties 这个结构体:symbol、magic、price_open、sl、tp、time_setup_msc 等 15 个成员,覆盖了从品种名到毫秒级下单时间的全量挂单元数据。注意 time_setup_msc 是 1970-01-01 起的毫秒数,做高频挂单去重时比秒级 time_setup 更稳。 代码里用全局变量 pending_order_properties ord 承接数据,再写 GetPendingOrderProperties(ENUM_ORDER_PROPERTIES) 按传入枚举分支赋值。比如 case O_SYMBOL 走 OrderGetString(ORDER_SYMBOL),case O_MAGIC 走 OrderGetInteger(ORDER_MAGIC)——你在 EA 里照这个骨架加 case,就能把选中挂单直接读进结构体供小布类脚本做风控。 外汇与贵金属挂单受点差跳空影响大,sl/tp 实际触发价可能偏离预设,用这套读取逻辑做本地校验时须留足滑点冗余。

MQL5 / C++
  O_VOLUME_CURRENT  = class="num">7,
  O_SL               = class="num">8,
  O_TP               = class="num">9,
  O_TIME_SETUP       = class="num">10,
  O_TIME_EXPIRATION = class="num">11,
  O_TIME_SETUP_MSC   = class="num">12,
  O_TYPE_TIME        = class="num">13,
  O_TYPE             = class="num">14,
  O_ALL              = class="num">15
  };
class=class="str">"cmt">//-- Properties of a pending order
class="kw">struct pending_order_properties
  {
   class="type">class="kw">string            symbol;         class=class="str">"cmt">// Symbol
   class="type">long              magic;          class=class="str">"cmt">// Magic number
   class="type">class="kw">string            comment;        class=class="str">"cmt">// Comment
   class="type">class="kw">double            price_open;     class=class="str">"cmt">// Price specified in the order
   class="type">class="kw">double            price_current;  class=class="str">"cmt">// Current price of the order symbol
   class="type">class="kw">double            price_stoplimit; class=class="str">"cmt">// Limit order price for the Stop Limit order
   class="type">class="kw">double            volume_initial; class=class="str">"cmt">// Initial order volume
   class="type">class="kw">double            volume_current; class=class="str">"cmt">// Current order volume
   class="type">class="kw">double            sl;             class=class="str">"cmt">// Stop Loss level
   class="type">class="kw">double            tp;             class=class="str">"cmt">// Take Profit level
   class="type">class="kw">datetime          time_setup;     class=class="str">"cmt">// Order placement time
   class="type">class="kw">datetime          time_expiration; class=class="str">"cmt">// Order expiration time
   class="type">class="kw">datetime          time_setup_msc; class=class="str">"cmt">// The time of placing an order for execution in milliseconds since class="num">01.01.class="num">1970
   class="type">class="kw">datetime          type_time;      class=class="str">"cmt">// Order lifetime
   ENUM_ORDER_TYPE   type;           class=class="str">"cmt">// Position type
   };
class=class="str">"cmt">//--- Variable of the order features
pending_order_properties ord;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Retrieves the properties of the previously selected pending order|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GetPendingOrderProperties(ENUM_ORDER_PROPERTIES order_property)
  {
   class="kw">switch(order_property)
     {
      case O_SYMBOL         : ord.symbol=OrderGetString(ORDER_SYMBOL);                                            class="kw">break;
      case O_MAGIC          : ord.magic=OrderGetInteger(ORDER_MAGIC);                                             class="kw">break;

◍ 用 switch 把订单字段搬进结构体

在 MT5 的订单遍历里,用 switch 按字段枚举逐个取数,比每次全量拷贝更省开销。下面这段把 OrderGetString / OrderGetDouble / OrderGetInteger 按类型分流,分别写进自定义结构体 ord 的对应成员。 注意 O_TIME_SETUP_MSC 这类毫秒级时间戳被强转成 datetime,精度会丢到秒;若你要做微秒级持仓分析,得自己留 long 原值。 O_ALL 分支则是一次性把 symbol、magic、comment 和几个价格字段全拉进来,适合初始化面板用。外汇和贵金属杠杆高,用这套读持仓做风控脚本时,注意价格字段实时跳动可能带来的滑点误判。

MQL5 / C++
case O_COMMENT         : ord.comment=OrderGetString(ORDER_COMMENT);                     class="kw">break;
case O_PRICE_OPEN       : ord.price_open=OrderGetDouble(ORDER_PRICE_OPEN);             class="kw">break;
case O_PRICE_CURRENT    : ord.price_current=OrderGetDouble(ORDER_PRICE_CURRENT);       class="kw">break;
case O_PRICE_STOPLIMIT  : ord.price_stoplimit=OrderGetDouble(ORDER_PRICE_STOPLIMIT);   class="kw">break;
case O_VOLUME_INITIAL   : ord.volume_initial=OrderGetDouble(ORDER_VOLUME_INITIAL);     class="kw">break;
case O_VOLUME_CURRENT   : ord.volume_current=OrderGetDouble(ORDER_VOLUME_CURRENT);     class="kw">break;
case O_SL               : ord.sl=OrderGetDouble(ORDER_SL);                             class="kw">break;
case O_TP               : ord.tp=OrderGetDouble(ORDER_TP);                             class="kw">break;
case O_TIME_SETUP       : ord.time_setup=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_SETUP);      class="kw">break;
case O_TIME_EXPIRATION  : ord.time_expiration=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_EXPIRATION); class="kw">break;
case O_TIME_SETUP_MSC   : ord.time_setup_msc=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_SETUP_MSC);  class="kw">break;
case O_TYPE_TIME        : ord.type_time=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TYPE_TIME);         class="kw">break;
case O_TYPE             : ord.type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);            class="kw">break;
case O_ALL              :
   ord.symbol=OrderGetString(ORDER_SYMBOL);
   ord.magic=OrderGetInteger(ORDER_MAGIC);
   ord.comment=OrderGetString(ORDER_COMMENT);
   ord.price_open=OrderGetDouble(ORDER_PRICE_OPEN);
   ord.price_current=OrderGetDouble(ORDER_PRICE_CURRENT);
   ord.price_stoplimit=OrderGetDouble(ORDER_PRICE_STOPLIMIT);

挂单结构体字段的抓取与下单封装

从订单池里取挂单属性时,要用 OrderGetDouble 拿浮点类字段、OrderGetInteger 拿整型/时间类字段,再强转成对应枚举或 datetime。下面这段把初始手数、当前手数、SL/TP、建单时间与过期时间一次性读进自定义结构体,注意 ORDER_TIME_SETUP_MSC 返回的是毫秒精度时间戳,转 datetime 后会丢掉亚秒部分。 ord.volume_initial=OrderGetDouble(ORDER_VOLUME_INITIAL); ord.volume_current=OrderGetDouble(ORDER_VOLUME_CURRENT); ord.sl=OrderGetDouble(ORDER_SL); ord.tp=OrderGetDouble(ORDER_TP); ord.time_setup=(datetime)OrderGetInteger(ORDER_TIME_SETUP); ord.time_expiration=(datetime)OrderGetInteger(ORDER_TIME_EXPIRATION); ord.time_setup_msc=(datetime)OrderGetInteger(ORDER_TIME_SETUP_MSC); ord.type_time=(datetime)OrderGetInteger(ORDER_TYPE_TIME); ord.type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); 读到未知枚举分支时直接 Print 提示并 return,能避免脏数据进后续逻辑。实盘跑 EURUSD 挂单时若发现 time_setup_msc 与 time_setup 差好几分钟,通常是历史订单缓存,不是行情问题。 下单封装函数 SetPendingOrder 把 symbol_number、order_type、lot、stoplimit_price、price、sl、tp、type_time、comment 全参数化,内部先 trade.SetExpertMagicNumber(MagicNumber) 再调 trade.OrderOpen。外汇和贵金属挂单受点差与流动性影响,滑点可能让挂单成交价偏离 price,属高风险操作,建议先在策略测试器用 2023 年 XAUUSD 数据验证。

MQL5 / C++
ord.volume_initial=OrderGetDouble(ORDER_VOLUME_INITIAL);
ord.volume_current=OrderGetDouble(ORDER_VOLUME_CURRENT);
ord.sl=OrderGetDouble(ORDER_SL);
ord.tp=OrderGetDouble(ORDER_TP);
ord.time_setup=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_SETUP);
ord.time_expiration=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_EXPIRATION);
ord.time_setup_msc=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TIME_SETUP_MSC);
ord.type_time=(class="type">class="kw">datetime)OrderGetInteger(ORDER_TYPE_TIME);
ord.type=(ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
class=class="str">"cmt">//---
class="kw">default: Print("Retrieved feature of the pending order was not taken into account in the enumeration "); class="kw">return;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Places a pending order                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetPendingOrder(class="type">int               symbol_number,    class=class="str">"cmt">// Symbol number
                     ENUM_ORDER_TYPE   order_type,       class=class="str">"cmt">// Order type
                     class="type">class="kw">double             lot,              class=class="str">"cmt">// Volume
                     class="type">class="kw">double             stoplimit_price, class=class="str">"cmt">// Level of the StopLimit order
                     class="type">class="kw">double             price,            class=class="str">"cmt">// Price
                     class="type">class="kw">double             sl,               class=class="str">"cmt">// Stop Loss
                     class="type">class="kw">double             tp,               class=class="str">"cmt">// Take Profit
                     ENUM_ORDER_TYPE_TIME type_time,      class=class="str">"cmt">// Order Expiration
                     class="type">class="kw">string             comment)          class=class="str">"cmt">// Comment
class=class="str">"cmt">//--- Set magic number in the trade structure
trade.SetExpertMagicNumber(MagicNumber);
class=class="str">"cmt">//--- If a pending order failed to be placed, print an error message
if(!trade.OrderOpen(Symbols[symbol_number],

「挂单改仓的删单重挂逻辑」

修改挂单时若传入的手数 volume 大于 0,代码选择先删旧单再下新单,而不是走 OrderModify 原地改参。这种思路在跨品种批量调度里更不容易卡在部分成交状态,但代价是 ticket 会变,依赖旧 ticket 的后续逻辑要先接住 DeletePendingOrder 的返回值。 具体看这段:if(volume>0) 成立后才进分支,先判 if(!DeletePendingOrder(ticket)) return; 删单失败直接退出,不继续。删成功后调 SetPendingOrder 按新 volume 重挂,并跑 CorrectStopLossByOrder 按订单类型和挂单价重新校准止损位。 外汇与贵金属杠杆高,重挂瞬间若碰报价跳空,stoplimit_price 与 price 可能同时越界,MT5 会直接报 4753 类错误。建议你在策略测试器里把 volume 改成随机非零值跑一轮,观察 ticket 刷新频率与成交滑点。

MQL5 / C++
class="type">void ModifyPendingOrder(class="type">int               symbol_number,   class=class="str">"cmt">//Symbol number
                         class="type">class="kw">ulong              ticket,           class=class="str">"cmt">// Order ticket
                         ENUM_ORDER_TYPE    type,             class=class="str">"cmt">// Order type
                         class="type">class="kw">double             price,            class=class="str">"cmt">// Order price
                         class="type">class="kw">double             sl,               class=class="str">"cmt">// Stop Loss of the order
                         class="type">class="kw">double             tp,               class=class="str">"cmt">// Take Profit of the order
                         ENUM_ORDER_TYPE_TIME type_time,       class=class="str">"cmt">// Order expiration
                         class="type">class="kw">datetime           time_expiration,  class=class="str">"cmt">// Order expiration time
                         class="type">class="kw">double             stoplimit_price,  class=class="str">"cmt">// Price
                         class="type">class="kw">string             comment,          class=class="str">"cmt">// Comment
                         class="type">class="kw">double             volume)           class=class="str">"cmt">// Volume
  {
class=class="str">"cmt">//--- If the passed volume value is non-zero, class="kw">delete the order and place it again
   if(volume>class="num">0)
     {
      class=class="str">"cmt">//--- If the order failed to be deleted, exit
      if(!DeletePendingOrder(ticket))
         class="kw">return;
      class=class="str">"cmt">//--- Place a pending order
      SetPendingOrder(symbol_number,type,volume,class="num">0,price,sl,tp,type_time,comment);
      class=class="str">"cmt">//--- Adjust Stop Loss positions as related to the order
      CorrectStopLossByOrder(symbol_number,price,type);
     }

◍ 挂单改价失败后的止损联动处理

当传入的成交量参数为零时,逻辑进入挂单修改分支:若 trade.OrderModify 返回 false,说明本次改价失败,直接把错误码和描述打到日志,便于在 MT5 Experts 标签里定位是无效价格还是超时。 若修改成功,则调用 CorrectStopLossByOrder,按挂单方向把对应持仓的止损重新对齐到新挂单价。外汇与贵金属点差跳动快,这种联动能降低手动漏改 SL 的概率,但行情跳空时仍可能滑点成交,属高风险操作。 DeletePendingOrder 只做一件事:调 trade.OrderDelete,失败就返回 false 并留错误日志,成功返回 true,调用方据此决定是否继续后续流程。 CorrectStopLossByOrder 内部先判 StopLoss[symbol_number]==0 直接退出;否则取 point、digits、持仓 TP,再按 BUY_STOP 在价上加止损、SELL_STOP 在价上减止损,最后用 PositionModify 写入。注意 new_sl 必须经过 NormalizeDouble 对齐品种小数位,否则 modify 会报 4756 无效参数。

MQL5 / C++
else
  {
   if(!trade.OrderModify(ticket,price,sl,tp,type_time,time_expiration,stoplimit_price))
     Print("Error when modifying the pending order price: ",
     GetLastError()," - ",ErrorDescription(GetLastError()));
   else
     CorrectStopLossByOrder(symbol_number,price,type);
  }
}
class="type">bool DeletePendingOrder(class="type">class="kw">ulong ticket)
  {
   if(!trade.OrderDelete(ticket))
     {
      Print("Error when deleting a pending order: ",GetLastError()," - ",ErrorDescription(GetLastError()));
      class="kw">return(class="kw">false);
     }
   class="kw">return(true);
  }
class="type">void CorrectStopLossByOrder(class="type">int               symbol_number,
                            class="type">class="kw">double            price,
                            ENUM_ORDER_TYPE   type)
  {
   if(StopLoss[symbol_number]==class="num">0)
     class="kw">return;
   class="type">class="kw">double new_sl=class="num">0.0;
   GetSymbolProperties(symbol_number,S_POINT);
   GetSymbolProperties(symbol_number,S_DIGITS);
   GetPositionProperties(symbol_number,P_TP);
   class="kw">switch(type)
     {
      case ORDER_TYPE_BUY_STOP  :
        new_sl=NormalizeDouble(price+CorrectValueBySymbolDigits(StopLoss[symbol_number]*symb.point),symb.digits);
        class="kw">break;
      case ORDER_TYPE_SELL_STOP :
        new_sl=NormalizeDouble(price-CorrectValueBySymbolDigits(StopLoss[symbol_number]*symb.point),symb.digits);
        class="kw">break;
     }
   if(!trade.PositionModify(Symbols[symbol_number],new_sl,pos.tp))
     Print("Error when modifying position: ",GetLastError()," - ",ErrorDescription(GetLastError()));
  }

按注释和品种筛挂单的两个底层函数

EA 管理挂单时,最怕重复下单或漏算某品种持仓。下面两段 MQL5 把『按注释查重』和『按品种统计挂单数』拆成独立函数,逻辑直接可抄进自己的脚本。 CheckPendingOrderByComment 接收品种序号和注释字符串,倒序遍历 OrdersTotal 返回的挂单池。它先用 OrderGetTicket 选单,再比 ORDER_SYMBOL 和 ORDER_COMMENT,命中即返回 true;全遍历完没撞上就返回 false,可作为『该注释挂单已存在』的判断开关。 OrdersTotalBySymbol 只认一个 symbol 参数,同样倒序扫池,但靠外部 GetOrderProperties(O_SYMBOL) 把属性填进 ord.symbol,相等就 count++。返回值是该品种当前挂单总数,实盘里用来限制同品种挂单上限很顺手。 外汇和贵金属杠杆高、滑点跳空频繁,这类查询函数务必在 OnTick 开头或定时器里轻量调用,避免在订单风暴时阻塞主线程。

MQL5 / C++
class="type">bool CheckPendingOrderByComment(class="type">int symbol_number,class="type">class="kw">string comment)
  {
   class="type">int   total_orders  =class="num">0;   class=class="str">"cmt">// 挂单总数
   class="type">class="kw">string order_symbol  =""; class=class="str">"cmt">// 订单品种
   class="type">class="kw">string order_comment =""; class=class="str">"cmt">// 订单注释
class=class="str">"cmt">//--- 获取挂单总数
   total_orders=OrdersTotal();
class=class="str">"cmt">//--- 遍历所有订单
   for(class="type">int i=total_orders-class="num">1; i>=class="num">0; i--)
     {
      class=class="str">"cmt">//--- 按索引选单
      if(OrderGetTicket(i)>class="num">0)
        {
         class=class="str">"cmt">//--- 取品种名
         order_symbol=OrderGetString(ORDER_SYMBOL);
         class=class="str">"cmt">//--- 品种一致才继续
         if(order_symbol==Symbols[symbol_number])
           {
            class=class="str">"cmt">//--- 取订单注释
            order_comment=OrderGetString(ORDER_COMMENT);
            class=class="str">"cmt">//--- 注释一致则返回存在
            if(order_comment==comment)
              class="kw">return(true);
           }
        }
     }
class=class="str">"cmt">//--- 未找到指定注释的挂单
   class="kw">return(class="kw">false);
  }

class="type">int OrdersTotalBySymbol(class="type">class="kw">string symbol)
  {
   class="type">int   count        =class="num">0; class=class="str">"cmt">// 计数器
   class="type">int   total_orders =class="num">0; class=class="str">"cmt">// 挂单总数
class=class="str">"cmt">//--- 获取挂单总数
   total_orders=OrdersTotal();
class=class="str">"cmt">//--- 遍历所有订单
   for(class="type">int i=total_orders-class="num">1; i>=class="num">0; i--)
     {
      class=class="str">"cmt">//--- 选单成功
      if(OrderGetTicket(i)>class="num">0)
        {
         class=class="str">"cmt">//--- 取订单品种属性到 ord.symbol
         GetOrderProperties(O_SYMBOL);
         class=class="str">"cmt">//--- 品种匹配则计数+class="num">1
         if(ord.symbol==symbol)
           count++;
        }
     }
class=class="str">"cmt">//--- 返回该品种挂单总数
   class="kw">return(count);
  }
把跨品种挂单状态交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到各限价单的触发与撤销轨迹,你只需核对时区参数是否和经纪商服务器对齐。

常见问题

StartTrade 是允许布置挂单的起始小时,StopOpenOrders 之后即便仓位平了也不再布新单,两者中间的窗口才是真正吃挂单触发的时间。
MQL5 的限价单不允许修改已挂单的手数字段,只能先 Delete 再按新手数 Place,否则请求会被终端拒绝。
可以,小布盯盘的品种页会汇总当前所有挂单与注释,跨品种遗漏的 Stop 单一眼能看出来,不用切图表翻日志。
启用 Reverse 且设了追踪位时,止损基于挂单自身价位推算,成交前随报价移动,成交后才转成持仓的追踪逻辑。
只要 EA 在线且事件循环正常,指定品种的所有持仓与挂单都会被清掉;若终端断开就需下次加载时补清理,外汇贵金属高风险,断线窗口要自己兜底。