跨平台订单管理器:从基类抽象打通 MT4/MT5 交易执行(基础篇)
(1/3)· 多数 EA 移植失败卡在交易函数差异,本篇先铺订单管理器的类骨架与职责边界
直接把 MQL4 的 EA 源文件丢进 MQL5 编译器,十有八九在 OrderSend 这关就挂掉。两个平台交易请求的结构和执行流程根本不是同一套逻辑,硬抄只会得到一堆报错和重复下单。本文是跨平台专家系统系列的第一部分,先把订单管理器的基类与设计目标讲清楚,后面两篇才谈得上实盘落地。
MT5 里先把订单管理拆清楚
在 MT5 做跨平台 EA,第一件事不是画信号,而是把订单生命周期管住。2017 年 6 月社区里一篇订单管理器帖拿到 11553 次浏览、3 条讨论,说明这块刚需一直没变:多品种、多魔术码下的持仓归并很容易写崩。 外汇与贵金属杠杆高,持仓偏移一个 tick 就可能放大成穿仓,所以管理器要先能按 symbol + magic 精确筛选,而不是遍历 AccountInfo 全量订单。 实际开 MT5 验证时,建议先只跑一个 XAUUSD 的 demo,把下单、追损、部分平仓三件事用独立函数封好,再谈跨平台。
◍ 这套下单封装到底覆盖了哪些点
想直接在 MT5 里复用一套统一下单逻辑,先得看清这个 COrder 类把哪些环节包了进去。从基类实现、交易量计算,到止损止盈算法、平仓与修改,再到 MQL4 与 MQL5 双规范兼容,目录里列了 16 个细分块。 其中「交易数量的计数」和「COrder 实例的存档」是容易被忽略的两项:前者决定每笔风险敞口是否超预设,后者关系到多单并发时对象状态不串味。 实际落地时,建议先只跑通 MQL5 规范下的开仓与平仓两个分支,用 0.01 手在 EURUSD 上验证;外汇与贵金属杠杆高,验证阶段务必用模拟盘。
「为什么 MQL4 代码不能直接搬进 MT5」
做过 MT4 到 MT5 迁移的人都知道,两个平台不是换个编译器就能跑通的。最扎眼的区别在交易执行层:MQL4 的下单逻辑和 MQL5 的异步订单/持仓模型根本不是一套,直接拿 .mq4 源文件用 MQL5 编译器编译,大概率满屏报错。 本文要落地的 COrderManager 类,就是专门用来填这个坑的。它和几个配套辅助类一起,接管 EA 里的下单、改单、撤单,以及持仓与历史交易的维护,把平台差异封在类内部,上层策略不必再关心 MT5 的异步返回码。 如果你手上有 MT4 的成熟策略想转 MT5,先别动信号逻辑,把交易执行这部分抽出来用类封装,是性价比最高的第一步。外汇与贵金属杠杆高,迁移后务必在策略测试器跑至少 3 个月 Tick 数据验证成交逻辑。
订单管理器要管的几件事
这套订单管理器在 EA 里只做六件具体活:算手数、定止损止盈、组装入场参数(过期时间、注释、魔幻数字)、跑发送前的预设条件、管当前订单、管历史订单。 手数和止损止盈的计算逻辑复杂且高度依赖策略,原文建议交给独立对象成员处理,不要写死在管理器里;而过期、注释、魔幻数字这类参数结构简单,由订单管理器自己吞掉更干净。 入场前的预设条件分两层:市场信号、时间窗、最大活跃单数、生命周期内总单数。后两个(最大活跃数和总单数)被塞进管理器发送订单前的最后关卡,能直接压住 EA 重复发单的毛病;前几个信号类条件放管理器外面更灵活。 外汇与贵金属杠杆高、滑点跳空频繁,把单数限制写进发送前而不是信号层,是降低多单踩踏风险的一种务实做法,值得在 MT5 里照这个边界拆一遍自己的 EA。
◍ 跨平台订单管理器的基类设计落点
做跨 MT4/MT5 的 EA,第一步不是写策略,而是把两套交易接口的差异压进一个基类。MT4 的 OrderSend 一次塞 11 个参数,MT5 把请求和结果收进 struct 只留两个入口,这是调用层最硬的分歧点。 平仓路径也分叉:MT4 用 OrderClose 关市价单,MT5 用 PositionClose 或反向同量请求。更麻烦的是账册记录——MT5 每笔动作留痕且 EA 可读,MT4 的订单 ID 从挂单到市价单不换号,完整轨迹只能翻日志,没有原生函数给程序抓取。 填充策略上 MT4 实质只有 FOK 等效行为,IOC 和 RETURN 在 MT4 不可控,所以跨平台代码统一锁死 ORDER_FILLING_FOK(也是 MT5 默认)。但要注意 SYMBOL_VOLUME_MAX:MT5 超量自动拆单,MT4 直接拒单,发单前必须自己先算手数上限。 基类里只放纯计算(手数、止损止盈、校验),具体发单函数做成虚方法在两个语言子类分别实现。COrder 实例存在 m_orders 活跃池与 m_orders_history 归档池,平仓后靠 ArchiveOrder 搬池子,这样 EA 不用依赖平台原生历史接口就能判断某单是否已离场。 外汇与贵金属杠杆高、滑点跳空频繁,跨平台兼容层若漏掉拆单检查,实盘可能在 MT4 上莫名撤单。开 MT5 把上面 MQL4 的 OrderSend 原型对照自己 EA 的封装层,确认参数映射没丢滑点与过期字段。
class="type">int OrderSend( class="type">class="kw">string symbol, class=class="str">"cmt">// 品种 class="type">int cmd, class=class="str">"cmt">// 操作 class="type">class="kw">double volume, class=class="str">"cmt">// 交易量 class="type">class="kw">double price, class=class="str">"cmt">// 价位 class="type">int slippage, class=class="str">"cmt">// 滑点 class="type">class="kw">double stoploss, class=class="str">"cmt">// 止损 class="type">class="kw">double takeprofit, class=class="str">"cmt">// 止盈 class="type">class="kw">string comment=NULL, class=class="str">"cmt">// 注释 class="type">int magic=class="num">0, class=class="str">"cmt">// 魔幻数字 class="type">class="kw">datetime expiration=class="num">0, class=class="str">"cmt">// 挂单过期 class="type">class="kw">color arrow_color=clrNONE class=class="str">"cmt">// 颜色 );
「把下单与风控计算塞进管理器基类」
订单管理器基类把开仓、手数、止损止盈的计算全部收口在一组方法里,外部只需传价格和订单类型,不用每次重写逻辑。LotSizeCalculate 先检查资金管理对象指针是否有效,有效就调 Volume 算手数,否则退回默认 m_lotsize;外汇和贵金属波动大,手数算错可能直接爆仓,这套分支能避免空指针带来的隐性风险。 StopLossCalculate 与 TakeProfitCalculate 同理,依赖 m_main_stop 的 Tick 换算;若对象为空就返回 0,意味着不下止损——实盘里这是高危动作,调用前务必确认指针已绑定。 Validate 方法遍历资金与止损两个子模块,任一校验失败整体返回 false,TradesTotal 则把当前挂单、历史订单和内部计数相加,一次调用就能拿到 17 这类合计值(取决于 m_history_count)。ArchiveOrder 只是把订单对象塞进历史列表,方便后续统计。 下面这段是基类核心方法的直接写法,复制进 MT5 的 include 里就能继承改。
class="type">bool OrderSend( class="type">MqlTradeRequest& request, class=class="str">"cmt">// 查询结构 class="type">MqlTradeResult& result class=class="str">"cmt">// 应答结构 ); COrder* COrderManagerBase::TradeOpen(class="kw">const class="type">class="kw">string,ENUM_ORDER_TYPE) { class="kw">return NULL; } class="type">class="kw">double COrderManagerBase::LotSizeCalculate(class="kw">const class="type">class="kw">double price,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double stoploss) { if(CheckPointer(m_moneys)) class="kw">return m_moneys.Volume(m_symbol.Name(),class="num">0,type,stoploss); class="kw">return m_lotsize; } class="type">class="kw">double COrderManagerBase::StopLossCalculate(class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double price) { if(CheckPointer(m_main_stop)) class="kw">return m_main_stop.StopLossTicks(type,price); class="kw">return class="num">0; } class="type">class="kw">double COrderManagerBase::TakeProfitCalculate(class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double price) { if(CheckPointer(m_main_stop)) class="kw">return m_main_stop.TakeProfitTicks(type,price); class="kw">return class="num">0; } class="type">bool COrderManagerBase::Validate(class="type">void) class="kw">const { if(CheckPointer(m_moneys)==POINTER_DYNAMIC) { if(!m_moneys.Validate()) class="kw">return class="kw">false; } if(CheckPointer(m_stops)==POINTER_DYNAMIC) { if(!m_stops.Validate()) class="kw">return class="kw">false; } class="kw">return true; } class="type">int COrderManagerBase::OrdersTotal(class="type">void) class="kw">const { class="kw">return m_orders.Total(); } class="type">int COrderManagerBase::OrdersHistoryTotal(class="type">void) class="kw">const { class="kw">return m_orders_history.Total(); } class="type">int COrderManagerBase::TradesTotal(class="type">void) class="kw">const { class="kw">return m_orders.Total()+m_orders_history.Total()+m_history_count; } class="type">int trades_total =TradesTotal(); class="type">int orders_total = OrdersTotal(); class="type">bool COrderManagerBase::ArchiveOrder(COrder *order) { class="kw">return m_orders_history.Add(order); }