轻松快捷开发 MetaTrader 程序的函数库 (第二十六部分):处理延后交易请求 - 首次实现 (开仓)·进阶篇

轻松快捷开发 MetaTrader 程序的函数库 (第二十六部分):处理延后交易请求 - 首次实现 (开仓)·进阶篇

(2/3)· 当交易请求遇错需等待,传统 Sleep 会让 EA 停滞,本篇给出可落地的延后请求开仓方案

偏理论进阶 第 2/3 篇
在 EA 里用 Sleep 等服务器错误恢复,往往让整个程序卡在交易方法里动弹不得。把请求丢进延后队列再退出方法,逻辑才能继续跑。这种非阻塞思路是写稳健交易库的关键一步。

「订单排序与消息枚举的底层字段」

在 MT5 的订单处理接口里,系统用一组枚举常量决定持仓或挂单的排列依据。从止损价、止盈价到浮动盈亏、佣金、库存费,再到成交量与未成交残留量,每一类属性都对应一个独立排序键,实盘里按品种批量巡检时可直接套用。 SORT_BY_ORDER_PROFIT_FULL 把利润、佣金和 swap 合并计算,比单纯看 profit 更贴近账户真实净值变动;而 SORT_BY_ORDER_VOLUME_CURRENT 专盯未执行部分,对部分成交的挂单特别有用。 字符串类排序从 FIRST_ORD_STR_PROP 起算,SORT_BY_ORDER_SYMBOL 排在最前,其后依次是注释、自定义注释与外系统订单 ID。写 EA 时若想按魔法号分组,得自己用循环筛,枚举本身不提供按 magic 排序的键。 消息结构体里 MSG_ORD_MAGIC_ID、MSG_ORD_GROUP_ID1、MSG_ORD_GROUP_ID2、MSG_ORD_PEND_REQ_ID 常被标黄,说明它们是订单推送通知的核心字段;MSG_ORD_PROFIT_PT 则以点数为单位回报利润,和报价货币计价的 profit 是两码事。

MQL5 / C++
  SORT_BY_ORDER_SL,                                                                       class=class="str">"cmt">// Sort by StopLoss price
  SORT_BY_ORDER_TP,                                                                       class=class="str">"cmt">// Sort by TakeProfit price
  SORT_BY_ORDER_PROFIT,                                                                   class=class="str">"cmt">// Sort by profit
  SORT_BY_ORDER_COMMISSION,                                                               class=class="str">"cmt">// Sort by commission
  SORT_BY_ORDER_SWAP,                                                                     class=class="str">"cmt">// Sort by swap
  SORT_BY_ORDER_VOLUME,                                                                   class=class="str">"cmt">// Sort by volume
  SORT_BY_ORDER_VOLUME_CURRENT,                                                           class=class="str">"cmt">// Sort by unexecuted volume
  SORT_BY_ORDER_PROFIT_FULL,                                                              class=class="str">"cmt">// Sort by profit+commission+swap
  SORT_BY_ORDER_PRICE_STOP_LIMIT,                                                         class=class="str">"cmt">// Sort by Limit order when StopLimit order is activated
class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties
  SORT_BY_ORDER_SYMBOL = FIRST_ORD_STR_PROP,                                              class=class="str">"cmt">// Sort by symbol
  SORT_BY_ORDER_COMMENT,                                                                  class=class="str">"cmt">// Sort by comment
  SORT_BY_ORDER_COMMENT_EXT,                                                              class=class="str">"cmt">// Sort by custom comment
  SORT_BY_ORDER_EXT_ID                                                                    class=class="str">"cmt">// Sort by order ID in an external trading system
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
  MSG_ORD_PROFIT_PT,                                                                       class=class="str">"cmt">// Profit in points
  MSG_ORD_MAGIC_ID,                                                                        class=class="str">"cmt">// Magic number ID
  MSG_ORD_GROUP_ID1,                                                                       class=class="str">"cmt">// First group ID
  MSG_ORD_GROUP_ID2,                                                                       class=class="str">"cmt">// Second group ID
  MSG_ORD_PEND_REQ_ID,                                                                     class=class="str">"cmt">// Pending request ID
  MSG_ORD_PRICE_OPEN,                                                                      class=class="str">"cmt">// Open price
  {"Прибыль в пунктах","Profit in points"},

◍ 把魔数拆成四段信息的位布局

MQL5 里订单的 magic number 是 ulong,但常被塞进 32 位整数里做多路复用。下面这段布局把 32 位按字节切开:低 16 位放原始 magic(ushort),字节 2 的低 4 位放第一组 ID、高 4 位放第二组 ID,字节 3 放挂单请求 ID。 这样同一个 magic 值就能同时承载「策略魔数 + 两组分类 + pending 请求标识」,回测时按位掩码提取,比另开字符串属性省内存。外汇与贵金属杠杆高,多策略并发下单时这种编码能降低订单归属误判概率。 GetMagicID 用 this.Magic() & 0xFFFF 直接截低 16 位,验证时你在 EA 里 Print 一下返回值,应等于你写入魔数的低 2 字节。

MQL5 / C++
class="type">class="kw">ushort GetMagicID(class="type">void) class="kw">const { class="kw">return class="type">class="kw">ushort(this.Magic() & 0xFFFF); }

从 Magic 号里抠出分组与挂单请求标识

在 EA 的订单管理类里,Magic 号不该只当过滤器用。把 32 位 magic 按位切开,能塞进多组元数据而不占用额外全局变量。 下面三个内联函数直接从 this.Magic() 取段:GetGroupID1 取右移 16 位后低 4 位(掩码 0x0F),GetGroupID2 取同段高 4 位再右移 4 位(掩码 0xF0),GetPendReqID 取右移 24 位后的整字节(掩码 0xFF)。也就是说,magic 的 16~23 位装两个 4 位分组 ID,24~31 位装一个 0~255 的挂单请求序号。 公开接口里 Ticket / TicketFrom / TicketTo 只是对 GetProperty 的薄封装,分别取 ORDER_PROP_TICKET、ORDER_PROP_TICKET_FROM、ORDER_PROP_TICKET_TO。回测时若同一 magic 下挂了 12 个改单请求,PendReqID 会循环 0~255,超 255 才回绕,实盘外汇与贵金属波动快,这种回绕可能引发订单归属误判,属高风险操作点。 开 MT5 把这段贴进你的 COrder 派生类,跑一单 EURUSD 测试,打印 Magic() 与各 Get* 返回值,验证位切割是否符合预期。

MQL5 / C++
class="type">uchar GetGroupID1(class="type">void) class="kw">const { class="kw">return class="type">uchar(this.Magic()>>class="num">16) & 0x0F; }
class="type">uchar GetGroupID2(class="type">void) class="kw">const { class="kw">return class="type">uchar((this.Magic()>>class="num">16) & 0xF0)>>class="num">4; }
class="type">uchar GetPendReqID(class="type">void) class="kw">const { class="kw">return class="type">uchar(this.Magic()>>class="num">24) & 0xFF; }
class="kw">public:
class=class="str">"cmt">//--- Default constructor
COrder(class="type">void){;}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Methods of a simplified access to the order object properties    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Return(class="num">1) ticket, (class="num">2) parent order ticket, (class="num">3) derived order ticket, (class="num">4) magic number, (class="num">5) order reason,
class=class="str">"cmt">//--- (class="num">6) position ID, (class="num">7) opposite position ID, (class="num">8) first group ID, (class="num">9) second group ID,
class=class="str">"cmt">//--- (class="num">10) pending request ID, (class="num">11) magic number ID, (class="num">12) type, (class="num">13) flag of closing by StopLoss,
class=class="str">"cmt">//--- (class="num">14) flag of closing by TakeProfit(class="num">15) open time, (class="num">16) close time,
class=class="str">"cmt">//--- (class="num">17) order expiration date, (class="num">18) state, (class="num">19) status, (class="num">20) type by direction
class="type">long Ticket(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_TICKET); }
class="type">long TicketFrom(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_TICKET_FROM); }
class="type">long TicketTo(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_TICKET_TO); }

「订单对象里的标识类属性怎么取」

在 MQL5 的订单封装类里,有一组只读方法专门回传订单的关联标识,全部走 GetProperty 统一接口,只是枚举常量不同。 Magic 取的是 EA 自己打的幻数,Reason 回传订单成因枚举(比如手动还是算法触发),PositionID 与 PositionByID 分别对应当前持仓 ID 和源持仓 ID,用来在部分平仓或对冲后做链路追踪。 下面这组是新版订单属性里容易被忽略的:MagicID、GroupID1、GroupID2 是扩展分组标识,PendReq_ID 指向挂单修改请求编号。它们让多策略同账户运行时,按组统计与过滤更干净,不至于全靠 Magic 一个字段硬扛。 外汇与贵金属杠杆高,用这些标识做仓位归组时,仍要留意同一账户多 EA 并发可能造成的 ID 冲突,回测和实盘都建议先打印一遍确认。

MQL5 / C++
class="type">long Magic(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_MAGIC); }
class="type">long Reason(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_REASON); }
class="type">long PositionID(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_POSITION_ID); }
class="type">long PositionByID(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_POSITION_BY_ID); }
class="type">long MagicID(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_MAGIC_ID); }
class="type">long GroupID1(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_GROUP_ID1); }
class="type">long GroupID2(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_GROUP_ID2); }
class="type">long PendReqID(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_PEND_REQ_ID); }
class="type">long TypeOrder(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_TYPE); }

◍ 从订单对象里抠出平仓原因与全成本

在 MT5 的自定义订单类里,判断一笔单子是不是被止损或止盈扫掉,靠的是两个布尔方法。IsCloseByStopLoss 读取 ORDER_PROP_CLOSE_BY_SL 属性,返回 true 即代表该笔仓位由止损触发平仓;IsCloseByTakeProfit 同理对应 ORDER_PROP_CLOSE_BY_TP。 开平时间也能直接拿:TimeOpen 取 ORDER_PROP_TIME_OPEN、TimeClose 取 ORDER_PROP_TIME_CLOSE,返回 long 型时间戳;挂单的过期时间走 TimeExpiration(ORDER_PROP_TIME_EXP),类型是 datetime。订单当前处于哪种生命周期,用 State 拿 ENUM_ORDER_STATE、Status 拿 ENUM_ORDER_STATUS 区分。 真正算账别只看 Profit()。ProfitFull 把浮动/实际盈利、佣金 Comission() 和库存费 Swap() 三项相加,才是这笔单子的全成本净利。外汇与贵金属杠杆高,库存费在周三往往翻三倍,跑回测时漏掉这一项,样本外结果可能明显偏离。 下面这段是从订单类摘出的方法定义,逐行对应上面说的属性: bool IsCloseByStopLoss(void) const { return (bool)this.GetProperty(ORDER_PROP_CLOSE_BY_SL); } // 返回是否由止损平仓 bool IsCloseByTakeProfit(void) const { return (bool)this.GetProperty(ORDER_PROP_CLOSE_BY_TP); } // 返回是否由止盈平仓 long TimeOpen(void) const { return this.GetProperty(ORDER_PROP_TIME_OPEN); } // 开仓时间(long 时间戳) long TimeClose(void) const { return this.GetProperty(ORDER_PROP_TIME_CLOSE); } // 平仓时间(long 时间戳) datetime TimeExpiration(void) const { return (datetime)this.GetProperty(ORDER_PROP_TIME_EXP); } // 挂单过期时间 ENUM_ORDER_STATE State(void) const { return (ENUM_ORDER_STATE)this.GetProperty(ORDER_PROP_STATE); } // 订单状态枚举 ENUM_ORDER_STATUS Status(void) const { return (ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS); } // 订单成交状态枚举 ENUM_ORDER_TYPE TypeByDirection(void) const { return (ENUM_ORDER_TYPE)this.GetProperty(ORDER_PROP_DIRECTION); } // 多空方向对应的订单类型 //--- Get the full order profit double ProfitFull(void) const { return this.Profit()+this.Comission()+this.Swap(); } // 总利润=盈利+佣金+库存费 //--- Get order profit in points int ProfitInPoints(void) const; // 以点数表示的利润(类外实现) 开 MT5 新建一个 EA 把这段贴进订单结构,打印 100 笔历史单的 ProfitFull 与 Profit 差值,就能直观看到佣金加库存费吃掉了几个点。

MQL5 / C++
class="type">bool IsCloseByStopLoss(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(ORDER_PROP_CLOSE_BY_SL); }
class="type">bool IsCloseByTakeProfit(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(ORDER_PROP_CLOSE_BY_TP); }
class="type">long TimeOpen(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_TIME_OPEN); }
class="type">long TimeClose(class="type">void) class="kw">const { class="kw">return this.GetProperty(ORDER_PROP_TIME_CLOSE); }
class="type">class="kw">datetime TimeExpiration(class="type">void) class="kw">const { class="kw">return (class="type">class="kw">datetime)this.GetProperty(ORDER_PROP_TIME_EXP); }
ENUM_ORDER_STATE State(class="type">void) class="kw">const { class="kw">return (ENUM_ORDER_STATE)this.GetProperty(ORDER_PROP_STATE); }
ENUM_ORDER_STATUS Status(class="type">void) class="kw">const { class="kw">return (ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS); }
ENUM_ORDER_TYPE TypeByDirection(class="type">void) class="kw">const { class="kw">return (ENUM_ORDER_TYPE)this.GetProperty(ORDER_PROP_DIRECTION); }
class=class="str">"cmt">//--- Get the full order profit
class="type">class="kw">double ProfitFull(class="type">void) class="kw">const { class="kw">return this.Profit()+this.Comission()+this.Swap(); }
class=class="str">"cmt">//--- Get order profit in points
class="type">int ProfitInPoints(class="type">void) class="kw">const;

订单对象的扩展属性与构造初始化

在自建订单类里,除了标准字段,还可以挂自定义分组与扩展备注。下面四个 setter 直接写进 ORDER_PROP_GROUP_ID1、GROUP_ID2、PEND_REQ_ID、COMMENT_EXT,等于给每笔单子打上你自己的标签,方便后面按策略簇或挂单请求 ID 做批量归并。 void SetGroupID1(const long group_id) { this.SetProperty(ORDER_PROP_GROUP_ID1,group_id); } void SetGroupID2(const long group_id) { this.SetProperty(ORDER_PROP_GROUP_ID2,group_id); } void SetPendReqID(const long req_id) { this.SetProperty(ORDER_PROP_PEND_REQ_ID,req_id); } void SetCommentExt(const string comment_ext) { this.SetProperty(ORDER_PROP_COMMENT_EXT,comment_ext); } 真正落单时靠带参构造函数收口。COrder::COrder(ENUM_ORDER_STATUS order_status,const ulong ticket) 里先把票据号存下,随后一口气把状态、魔术码、票据、过期时间、类型、状态、方向、关联持仓 ID、来源原因、成交单票据等 11 个 long 属性从当前订单对象拉进 m_long_prop 数组。 this.m_long_prop[ORDER_PROP_STATUS] = order_status; this.m_long_prop[ORDER_PROP_MAGIC] = this.OrderMagicNumber(); this.m_long_prop[ORDER_PROP_TICKET] = this.OrderTicket(); this.m_long_prop[ORDER_PROP_TIME_EXP] = this.OrderExpiration(); this.m_long_prop[ORDER_PROP_TYPE] = this.OrderType(); this.m_long_prop[ORDER_PROP_STATE] = this.OrderState(); this.m_long_prop[ORDER_PROP_DIRECTION] = this.OrderTypeByDirection(); this.m_long_prop[ORDER_PROP_POSITION_ID] = this.OrderPositionID(); this.m_long_prop[ORDER_PROP_REASON] = this.OrderReason(); this.m_long_prop[ORDER_PROP_DEAL_ORDER_TICKET] = this.DealOrderTicket(); 开 MT5 把这段塞进你的 EA 订单管理模块,跑一单黄金 XAUUSD 限价单,打印 m_long_prop 就能看到 11 个槽位全部被填实。外汇与贵金属杠杆高,这类内部索引若写错可能让平仓逻辑误杀持仓,验证时先用策略测试器小量回测。

MQL5 / C++
class="type">void SetGroupID1(class="kw">const class="type">long group_id) { this.SetProperty(ORDER_PROP_GROUP_ID1,group_id); }
class="type">void SetGroupID2(class="kw">const class="type">long group_id) { this.SetProperty(ORDER_PROP_GROUP_ID2,group_id); }
class="type">void SetPendReqID(class="kw">const class="type">long req_id) { this.SetProperty(ORDER_PROP_PEND_REQ_ID,req_id); }
class="type">void SetCommentExt(class="kw">const class="type">class="kw">string comment_ext) { this.SetProperty(ORDER_PROP_COMMENT_EXT,comment_ext); }

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Closed parametric constructor                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
COrder::COrder(ENUM_ORDER_STATUS order_status,class="kw">const class="type">class="kw">ulong ticket)
  {
class=class="str">"cmt">//--- Save integer properties
   this.m_ticket=ticket;
   this.m_long_prop[ORDER_PROP_STATUS]                                     = order_status;
   this.m_long_prop[ORDER_PROP_MAGIC]                                      = this.OrderMagicNumber();
   this.m_long_prop[ORDER_PROP_TICKET]                                     = this.OrderTicket();
   this.m_long_prop[ORDER_PROP_TIME_EXP]                                   = this.OrderExpiration();
   this.m_long_prop[ORDER_PROP_TYPE]                                       = this.OrderType();
   this.m_long_prop[ORDER_PROP_STATE]                                      = this.OrderState();
   this.m_long_prop[ORDER_PROP_DIRECTION]                                  = this.OrderTypeByDirection();
   this.m_long_prop[ORDER_PROP_POSITION_ID]                                = this.OrderPositionID();
   this.m_long_prop[ORDER_PROP_REASON]                                     = this.OrderReason();
   this.m_long_prop[ORDER_PROP_DEAL_ORDER_TICKET]                          = this.DealOrderTicket();
把请求队列交给小布盯盘
这些延后请求的生命周期诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到排队与触发状态,你只管判断策略时机。

常见问题

直接重试常在交易方法内 Sleep 阻塞,程序其他函数不可访问;延后请求把副本放入列表并退出方法,等待由类轮询触发,不中断主逻辑。
可以,小布盯盘对应品种页会呈现延后请求的等待与触发情况,省去自己写日志轮询的重复劳动。
魔幻数字随订单/仓位保存在服务器端,紧急重连后仍能凭此关联原请求,是轻量且抗中断的标识方式。
需记录触发价位与限价单参数,将限价下单设为延后请求,价格触达即发单,完整讨论见《轻松快捷开发 MetaTrader 程序的函数库·实战篇》。