MQL5 Cookbook - 以 MQL5 编写的多币种 EA,利用限价订单工作·进阶篇
(2/3)· 从模板到日内范式,6 节拆解多品种挂单的布置、反转与事件处理陷阱
「挂单属性的结构体与枚举映射」
在 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 实际触发价可能偏离预设,用这套读取逻辑做本地校验时须留足滑点冗余。
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 和几个价格字段全拉进来,适合初始化面板用。外汇和贵金属杠杆高,用这套读持仓做风控脚本时,注意价格字段实时跳动可能带来的滑点误判。
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 数据验证。
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 刷新频率与成交滑点。
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 无效参数。
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 开头或定时器里轻量调用,避免在订单风暴时阻塞主线程。
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); }