来自 MQL5 向导的预制专家交易系统运作于 MetaTrader 4 平台中(基础篇)
◍ MT4 向导为何缺了预制 EA 这一项
打开 MT4 和 MT5 的内置 MQL 向导,界面看着差不多,但 MT5 能直接勾选生成预制专家交易系统,MT4 里根本没有这个选项。根子出在标准库:MT5 向导依赖标准函数库里的交易类(下单、信号、追踪、资金管理),而 MT4 自带的 MQL5 子集只移植了部分 API,绕开了交易类,所以向导拼不出现成的 EA。 这和历史演进有关。MQL5 先长在 MT5 上,标准库类全为它写;后来语言才回灌进 MT4,但两边交易函数差异太大,只搬了非交易部分。MT4 现在早就不加新功能,只修 bug,想靠官方在向导里补窟窿基本没戏。 实务上还有条路:用 MT5 向导生成 EA,再把代码迁回 MT4。代价是要把 MT5 标准库里缺的交易类抄出来,按 MT4 原生 MQL API 重写,相当于在 MT4 里仿真一套 MT5 交易环境。外汇和贵金属杠杆高、滑点跳空频繁,这类移植 EA 未经实盘校验前,信号触发与订单执行可能偏离预期,务必先在策略测试器跑历史数据。 动手前得先搞懂 MT5 的订单、成交、仓位三件套——和 MT4 的订单即仓位逻辑不一样。不清楚的话,去翻终端里讲 MT5 订单成交仓位关系的那篇文档,否则抄类的时候连语义都对不上。
「把 MT5 交易类库搬进 MT4 的移植路线」
做跨终端代码移植,别一上来就写。MT4 标准库里没有 Trade、Expert、Models 三个文件夹,主任务就是把这些目录下的类整体搬过去,顺带改 Indicators 目录里因两端指标机制不同而产生的差异。 核心原则是尽量不碰官方库文件本身。函数库会随版本更新,你改得越多,后续和官方 diff 的成本越高,所以能靠外层封装和宏映射解决的,绝不去改原文件。 MT5 复制过来的代码大量依赖本端交易 API,所以要先仿一套接口一致的薄层,把调用转成 MT4 继承的 MQL API。搭建顺序很固定:枚举 → 结构 → 常数 → 函数,类型是盖楼的砖。 枚举里最坑的是 ENUM_ORDER_TYPE。MT5 比 MT4 多出了 BUY_STOP_LIMIT、SELL_STOP_LIMIT、CLOSE_BY 等类型,原样保留会编译报错,因为枚举不能重定义。直接用预处理器宏补上缺失项是最省事的。 结构部分从文档抄 MqlTradeRequest、MqlTradeResult、MqlTradeCheckResult 即可;常量则用 #define 铺量定义,比如 TRADE_RETCODE_*、SYMBOL_FILLING_* 等。函数和内核同名只是名字略不同,最后用简单宏替换就能接上。 外汇与贵金属交易本身高风险,这类底层移植若用于实盘,任何模型映射偏差都可能导致订单行为异常,验证前务必在策略测试器跑通。
class=class="str">"cmt">// ENUM_ORDER_TYPE 扩展 class="macro">#define ORDER_TYPE_BUY_STOP_LIMIT((ENUM_ORDER_TYPE)class="num">6) class="macro">#define ORDER_TYPE_SELL_STOP_LIMIT((ENUM_ORDER_TYPE)class="num">7) class="macro">#define ORDER_TYPE_CLOSE_BY((ENUM_ORDER_TYPE)class="num">8) enum ENUM_ORDER_TYPE_FILLING { ORDER_FILLING_FOK, ORDER_FILLING_IOC, ORDER_FILLING_RETURN }; class="macro">#define MQL5InfoInteger MQLInfoInteger class="macro">#define MQL5InfoString MQLInfoString
MT4 订单与 MT5 仓位的映射差异
从 MT4 起步做跨平台实体映射更顺手,因为它的“订单”是个兜底概念,把市场单、挂单和历史记录全裹在一起,只是状态不同。MT5 则拆得干净:市场单叫仓位,挂单叫订单,历史里是成交。 模拟 MT5 环境时,一笔 MT4 市场订单会展开成这条链:入场订单 → 入场成交 → 仓位 → 离场订单 → 离场成交。也就是说,MT4 里的一笔动作在 MT5 术语下至少跨越三个实体。 MT5 原生是净额平台,同一品种同时只能有一笔仓位,同向加仓、反向平仓都在总交易量上做增减,止损止盈也是挂在整个品种上。MT4 没有这模式,每笔订单执行后都独立成“仓位”,同品种可并存多笔甚至反向未平单。 想拿 MT5 和 MT4 的 EA 跑对照实验,先把 MT5 终端的对冲模式打开,否则净额逻辑会让两边成交量对不上。最好接同一经纪商服务器,减少点差和成交规则带来的干扰。外汇与贵金属杠杆高,跨平台回测结论仅作逻辑验证,实盘仍可能偏离。
◍ 在 MT4 内核上桥接 MT5 交易请求
把 MT5 的 OrderSend 映射回 MT4 的市价单与挂单,核心是把 MqlTradeRequest 里的 type 翻译成 MT4 的 OP_* 命令字。下面这段桥接函数就是干这个的:用 switch 把 ORDER_TYPE_BUY / SELL / BUY_LIMIT 等逐一对应到 OP_BUY、OP_BUYLIMIT,遇到 MT4 不支持的类型直接 Print 并返回 false。 平仓逻辑在 MT5 里是用反向单并填 position 字段,而 MT4 每笔订单自带仓位、单号即仓位号,所以桥接层在 position 非空时复用 OrderClose,并把虚拟订单原始编号的高位置 1 当作离场单号,后续枚举订单和成交时靠这个位来区分虚实。 标准库改动里有个硬约束:MT5 内核已内置 OrderGetString 等,不能在头文件里覆盖,否则编译报“覆盖系统函数”。解决办法是所有挂单读取函数加 PendingOrder_ 前缀,再全局替换标准库里的调用。OrderGetTicket 因 MT4 本来没有,可直接保留原名。 历史侧也得改造:MT4 一笔市价单在 MT5 历史里拆成入场、离场两笔订单加一对成交,数组容量要乘 2;挂单原样存,但占两个号位、奇数位留空。离场成交单号同样补高位,HistoryDealSelect 读取时要跳过这个高位偏移。外汇与贵金属杠杆高,桥接层任何单号错位都可能导致平仓失败,建议在 MT5 用模拟账户先跑通再上真实环境。
class="type">bool OrderSend(class="type">MqlTradeRequest &request, class="type">MqlTradeResult &result) { class="type">int cmd; result.retcode = class="num">0; class="kw">switch(request.type) { case ORDER_TYPE_BUY: cmd = OP_BUY; class="kw">break; case ORDER_TYPE_SELL: cmd = OP_SELL; class="kw">break; case ORDER_TYPE_BUY_LIMIT: cmd = OP_BUYLIMIT; class="kw">break; case ORDER_TYPE_SELL_LIMIT: cmd = OP_SELLLIMIT; class="kw">break; case ORDER_TYPE_BUY_STOP: cmd = OP_BUYSTOP; class="kw">break; case ORDER_TYPE_SELL_STOP: cmd = OP_SELLSTOP; class="kw">break; class="kw">default: Print("Unsupported request type:", request.type); class="kw">return false; } ResetLastError();
「成交与改单的底层分支处理」
这段逻辑跑在交易请求分发之后,核心是按 action 类型走不同通道:市价成交、挂单、改 SLTP、改挂单。外汇与贵金属杠杆高,任何下单通道的报错都得当场捕获,否则策略会在实盘里静默失效。 先看市价类动作。若请求价给的是 0,就按买卖方向用 MarketInfo 抓实时 ASK 或 BID 填坑;若指定了持仓号就走 OrderClose 平仓,否则 OrderSend 开新仓。平仓成功会把持仓号高位打上 0x8000000000000000 标记写回 deal 和 order,开仓失败则直接把 GetLastError 塞进 retcode。 改已开仓的 SLTP 走 OrderSelect + OrderModify,只动止损止盈不动开仓价。若选不到 ticket,retcode 置为 TRADE_RETCODE_POSITION_CLOSED——意味着持仓可能已被外部平掉。改挂单的分支在源码里被截断,但结构同改仓:先选中再改价,失败取错误码。 开 MT5 把这段塞进你的交易封装函数,故意传一个不存在的 position 号,看 retcode 是否落在 POSITION_CLOSED 而非 DONE,能立刻验证通道健壮性。
if(request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING) { if(request.price == class="num">0) { if(cmd == OP_BUY) { request.price = MarketInfo(request.symbol, MODE_ASK); } else if(cmd == OP_SELL) { request.price = MarketInfo(request.symbol, MODE_BID); } } if(request.position > class="num">0) { if(!OrderClose((class="type">int)request.position, request.volume, request.price, (class="type">int)request.deviation)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = request.position | 0x8000000000000000; result.order = request.position | 0x8000000000000000; result.volume = request.volume; result.price = request.price; } } else { class="type">int ticket = OrderSend(request.symbol, cmd, request.volume, request.price, (class="type">int)request.deviation, request.sl, request.tp, request.comment, (class="type">int)request.magic, request.expiration); if(ticket == -class="num">1) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = ticket; result.order = ticket; result.request_id = ticket; if(OrderSelect(ticket, SELECT_BY_TICKET)) { result.volume = OrderLots(); result.price = OrderOpenPrice() > class="num">0 ? OrderOpenPrice() : request.price; result.comment = OrderComment(); result.ask = MarketInfo(OrderSymbol(), MODE_ASK); result.bid = MarketInfo(OrderSymbol(), MODE_BID); } else { result.volume = request.volume; result.price = request.price; result.comment = ""; } } } } else if(request.action == TRADE_ACTION_SLTP) class=class="str">"cmt">// 修改已开持仓 { if(OrderSelect((class="type">int)request.position, SELECT_BY_TICKET)) { if(!OrderModify((class="type">int)request.position, OrderOpenPrice(), request.sl, request.tp, class="num">0)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = OrderTicket(); result.order = OrderTicket(); result.request_id = OrderTicket(); result.volume = OrderLots(); result.comment = OrderComment(); } } else { result.retcode = TRADE_RETCODE_POSITION_CLOSED; } } else if(request.action == TRADE_ACTION_MODIFY) class=class="str">"cmt">// 修改挂单 {