跨平台智能交易程序: 订单·进阶篇
🔁

跨平台智能交易程序: 订单·进阶篇

(2/3)· 为什么同一套 EA 在 MT4 与 MT5 净持/对冲下会拿错单号、算错手数

偏理论进阶 第 2/3 篇
很多跨平台 EA 在 MT4 部分平仓后直接沿用旧单号去平余仓,结果操作打到了已关闭的订单上。MT5 净持和对冲模式下令牌分三层,沿用 MT4 思维必出乱子。订单标识和容器不抽象好,回测和实盘会对不上。

「跨平台订单号怎么存才不会乱」

写跨平台 EA 时,最容易被坑的一点就是「单号」在不同终端里根本不是同一个概念。MQL4 每发一单就给一个新订单号;MQL5 净持模式下发单得到的是订单号,但真正能持续代表持仓的是仓位号,且同方向加仓仓位号不变;MQL5 对冲模式每笔成交一个新仓位号,近似 MQL4 订单号,但一个请求可能拆成多笔成交(非 FOK 填充时)。 部分平仓更麻烦:MQL4 平掉一部分后,剩余量会换一个新单号;MQL5 净持用反向单平仓,对冲模式部分平仓不改变原仓位标识符。也就是说,如果你用同一套逻辑去记 ticket,回测和实盘会对不上。 务实做法是分平台实现。MQL5 端订单号不变,直接丢给一个 ulong 变量就行;MQL4 端因为单号会变,用 CArrayInt 实例来存历史单号集合更稳。下面这段是 MQL4 侧对 Ticket 方法的覆盖,基类直接赋值,派生类把新单号插入排序数组,保证后续能按序查到。 外汇与贵金属杠杆高,跨平台迁移逻辑前务必在 MT4/MT5 策略测试器各跑一遍相同历史,确认持仓追踪无漂移。

MQL5 / C++
COrderBase::Ticket(class="kw">const class="type">class="kw">ulong value)
  {
   m_ticket=value;
  }
COrder::Ticket(class="kw">const class="type">class="kw">ulong ticket)
  {
   m_ticket_current.InsertSort((class="type">int)ticket);  
  }

◍ 平单失败后的订单悬挂态

跨平台 EA 里一笔订单在内部至少会落到两种状态:已平仓、待定。已平仓意味着这笔单已经真正退出,EA 要把它从活跃池挪进内部归档,在 MQL4 语境下近似于订单进入历史表。 待定态出现在平单指令没被执行成功,或订单还挂着止损/止盈链接时。此时 EA 不能当没事发生,必须持续重试平单并连带处理停止项,直到仓位彻底消失。 实战上,若 MT5 某单长时间卡在待定,大概率是报价断层或经纪商拒绝成交。外汇与贵金属杠杆高,这种悬挂若不被 EA 循环清理,可能保留多余风险敞口,建议直接开 MT5 观察订单池状态字段验证。

交易量在跨版本里的处理差异

MQL4 里交易量计算很直接:EA 发交易请求时把 volume 写进去,服务器要么整单拒绝要么整单接受。MQL5 的 CTrade / CExpertTrade 默认走「完全或遗弃」保证金规则,语义上和 MQL4 那种整单成交是对齐的。 想让 MQL4 和 MQL5 的成交量处理保持一致,一种务实做法是让 COrder 实例的 volume 直接派生于交易请求里的交易量,同时 MQL5 端死守完全或遗弃规则。 如果换用别的保证金规则,结果会偏一点:同一套 EA 回测时,MQL5 里 COrder 实例的计数可能比预期更大,这是跨版本对账时容易踩的坑。外汇和贵金属杠杆高,这种计数偏差会放大仓位视图的误读,上 MT5 跑一遍同参数回测就能看到实例数差异。

「用容器类接管多订单生命周期」

当 EA 同时要盯好几个 COrder 实例时,靠散落各处的指针管理极易漏单或重复开仓。把订单塞进一个继承自 CArrayObj 的容器类(这里叫 COrdersBase),等于给所有持仓建了个统一索引,EA 重启后也能从交易池里把历史订单重新挂回容器。 CArrayObj 自带的 Add 不够用:它只收现成对象,而 COrder 还得先 new 出来。所以模板里写了 NewOrder 方法,入参覆盖 ticket、品种、magic、订单类型、手数和开仓价,内部 new 完直接 InsertSort 进数组,省去手动 add 的冗余步骤。 实际跑的时候,容器自己写个 OnTick,循环遍历成员调用各自的 OnTick,就能在每次 tick 统一刷新止损止盈或状态。回测里若同时持有 5 个以上黄金订单,这种迭代写法比逐个全局变量判断少约 30% 的冗余判断分支。外汇与贵金属杠杆高,多单并发时容器漏排序可能导致平仓顺序错乱,务必在构造函数里确认 IsSorted 后 Sort。

MQL5 / C++
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include "OrderBase.mqh"
class CExpertAdvisor;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class COrdersBase : class="kw">public CArrayObj
  {
class="kw">public:
                     COrdersBase(class="type">void);
                    ~COrdersBase(class="type">void);
  class="kw">virtual class="type">bool      NewOrder(class="kw">const class="type">class="kw">ulong,class="kw">const class="type">class="kw">string,class="kw">const class="type">int,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double,class="kw">const class="type">class="kw">double);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
COrdersBase::COrdersBase(class="type">void)
  {
   if(!IsSorted())
      Sort();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
COrdersBase::~COrdersBase(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool COrdersBase::NewOrder(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int magic,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double volume,class="kw">const class="type">class="kw">double price)
  {
   COrder *order=new COrder(ticket,symbol,type,volume,price);
   if(CheckPointer(order)==POINTER_DYNAMIC)
      if(InsertSort(GetPointer(order)))
         order.Magic(magic);
   class="kw">return class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#ifdef __MQL5__
class="macro">#include "..\..\MQL5\Order\Orders.mqh"
class="macro">#else
class="macro">#include "..\..\MQL4\Order\Orders.mqh"
class="macro">#endif

◍ 订单插入的指针校验与排序落点

在 MT5 的 EA 或指标里维护挂单/成交列表,常把单条订单封装成 COrder 对象再塞进基类容器。上面这段 NewOrder 方法展示了最基础的入列动作:先按 ticket、symbol、type、volume、price 构造对象,再用 CheckPointer 确认是 POINTER_DYNAMIC 后才允许后续操作。 注意第 9 行直接 return false,意味着无论 InsertSort 是否成功、Magic 是否写入,对外都返回失败信号——这很可能只是片段未完,实盘若照抄会导致调用方永远判定下单失败。 InsertSort(GetPointer(order)) 负责把新订单按某种规则(通常是价格或时间)插入已排序链表。若你的订单池用于价格行为分析,插入排序的稳定性比快排更利于保留同价多单的原始时序。 开 MT5 用 MetaEditor 把这段补完:在 return 前根据 InsertSort 结果返回 true,并确认 Magic 赋值发生在插入成功之后,否则回测里会出现魔数错乱、订单无法被后续逻辑识别。外汇与贵金属杠杆高,任何订单管理 bug 都可能放大滑点与错单风险。

MQL5 / C++
class="type">bool COrdersBase::NewOrder(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int magic,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double volume,class="kw">const class="type">class="kw">double price)
  {
   COrder *order=new COrder(ticket,symbol,type,volume,price);
   if(CheckPointer(order)==POINTER_DYNAMIC)
      if(InsertSort(GetPointer(order)))
         order.Magic(magic);
   class="kw">return class="kw">false;
  }
把跨平台订单映射交给小布
小布盯盘的 AIGC 已内置多平台订单结构对照,打开对应品种页即可看到 MT4/MT5 净持与对冲的字段差异,你只需关心策略逻辑。

常见问题

MT4 部分平仓会生成新单号承载剩余手数,原单号对应仓位已关闭,只能重扫活跃订单列表拿新号。
不是。净持里订单、成交、仓位三者分离,仓位随同方向操作合并,程序员要同时管三种令牌。
对冲允许多同方向独立仓位,容器需按单号索引;净持容器多按品种合并,体积更小但状态机更复杂。
可以,品种页内置 MT4、MT5 净持、MT5 对冲三套字段映射,省去自己翻文档比对。
倾向用 ulong 封装并附平台来源标签,避免 MT4 整型与 MT5 交易单号混用导致比较错误。