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