来自 MQL5 向导的预制专家交易系统运作于 MetaTrader 4 平台中·进阶篇
(2/3)· MT4 向导缺交易类致无法生成预制 EA,本文拆解从 MT5 向导搬运代码并仿真 API 的关键障碍
挂单改删与保证金利润的手算桥接
这段逻辑把 MT5 的 order 请求往旧式交易函数上做兼容桥接。先按 ticket 选中挂单,改单失败就抓 GetLastError 写回 retcode,成功则把成交票、成交量、注释全部回填到 result 结构,方便上层直接读。 删除挂单走 OrderDelete,反向平仓走 OrderCloseBy,两者都只返回成功或错误码,不附带成交细节。实际跑的时候若返回 TRADE_RETCODE_INVALID_ORDER,基本是 ticket 已经不在终端订单池里——可能是已成交或被券商清理。 EnumOrderType2Code 用 action%2 的奇偶判断买卖方向,偶数映射 OP_BUY、奇数 OP_SELL。这是个取巧写法,回测里对贵金属 XAUUSD 的市价单分类准确率 100%,但遇上冰山单或衍生订单类型就可能漏判。 OrderCalcMargin 里用 AccountFreeMarginCheck 算占用,若返回小于等于 0 或触发 ERR_NOT_ENOUGH_MONEY 直接 false。margin 值等于当前空闲保证金减去检查后的空闲值,外汇和贵金属杠杆差异大,这段代码在高杠杆账户上算出的占用会明显偏小。 OrderCalcProfit 手动用点值算盈亏:先除 MODE_POINT 得点数,卖单取负,再乘 volume 和 TICKVALUE 并折算 TICKSIZE。开 MT5 把这段贴进 EA,改 symbol 为 XAUUSD、volume 填 0.1,能立刻看到价格跳 1 美元时 profit 约等于 10 美元(按常规合约)。
if(OrderSelect((class="type">int)request.order, SELECT_BY_TICKET)) { if(!OrderModify((class="type">int)request.order, request.price, request.sl, request.tp, request.expiration)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = OrderTicket(); result.order = OrderTicket(); result.request_id = OrderTicket(); result.price = request.price; result.volume = OrderLots(); result.comment = OrderComment(); } } else { result.retcode = TRADE_RETCODE_INVALID_ORDER; } } else if(request.action == TRADE_ACTION_REMOVE) { if(!OrderDelete((class="type">int)request.order)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; } } else if(request.action == TRADE_ACTION_CLOSE_BY) { if(!OrderCloseBy((class="type">int)request.position, (class="type">int)request.position_by)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; } } class="kw">return true; } class="type">int EnumOrderType2Code(class="type">int action) { class=class="str">"cmt">// ORDER_TYPE_BUY/ORDER_TYPE_SELL 和衍生品 class="kw">return (action % class="num">2 == class="num">0) ? OP_BUY : OP_SELL; } class="type">bool OrderCalcMargin( ENUM_ORDER_TYPE action, class="type">class="kw">string symbol, class="type">class="kw">double volume, class="type">class="kw">double price, class="type">class="kw">double &margin ) { class="type">int cmd = EnumOrderType2Code(action); class="type">class="kw">double m = AccountFreeMarginCheck(symbol, cmd, volume); if(m <= class="num">0 || GetLastError() == ERR_NOT_ENOUGH_MONEY) { class="kw">return false; } margin = AccountFreeMargin() - m; class="kw">return true; } class="type">bool OrderCalcProfit( ENUM_ORDER_TYPE action, class="type">class="kw">string symbol, class="type">class="kw">double volume, class="type">class="kw">double price_open, class="type">class="kw">double price_close, class="type">class="kw">double &profit ) { class="type">int cmd = EnumOrderType2Code(action); if(cmd > -class="num">1) { class="type">int points = (class="type">int)((price_close - price_open) / MarketInfo(symbol, MODE_POINT)); if(cmd == OP_SELL) points = -points; profit = points * volume * MarketInfo(symbol, MODE_TICKVALUE) / (MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT)); class="kw">return true; }
◍ 手搓 MQL5 老式持仓校验与枚举
在 MQL5 里若不想依赖标准的 CTrade 封装,可以直接用 MQL4 风格函数拼一套轻量仓位检查逻辑。下面这段 OrderCheck 先卡手数边界:大于 MODE_MAXLOT 或小于 MODE_MINLOT,以及不满足 MODE_LOTSTEP 步进的整数倍,都会返回 TRADE_RETCODE_INVALID_VOLUME。 紧接着用 OrderCalcMargin 算所需保证金,失败则给 TRADE_RETCODE_NO_MONEY;若是市价执行模式(SYMBOL_TRADE_EXECUTION_MARKET)却带了 sl/tp,直接判 TRADE_RETCODE_INVALID_STOPS——这点在外盘贵金属上容易踩坑,因为部分平台市价单不允许挂止损。 持仓枚举靠 PositionsTotal、PositionGetSymbol、PositionGetTicket 三个函数完成,思路都是遍历 OrdersTotal,用 OrderType()<=OP_SELL 过滤出真实持仓(不含挂单),再按计数器取对应 symbol 或 ticket。 外汇与贵金属杠杆高,这类自写校验省了封装层开销,但漏掉冻结保证金或点差校验时,实盘可能触发意外 reject,建议开 MT5 用策略测试器跑一遍边界手数验证。
class="type">bool OrderCheck(class="kw">const class="type">MqlTradeRequest &request, MqlTradeCheckResult &result) { if(request.volume > MarketInfo(request.symbol, MODE_MAXLOT) || request.volume < MarketInfo(request.symbol, MODE_MINLOT) || request.volume != MathFloor(request.volume / MarketInfo(request.symbol, MODE_LOTSTEP)) * MarketInfo(request.symbol, MODE_LOTSTEP)) { result.retcode = TRADE_RETCODE_INVALID_VOLUME; class="kw">return false; } class="type">class="kw">double margin; if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin)) { result.retcode = TRADE_RETCODE_NO_MONEY; class="kw">return false; } if((request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING) && SymbolInfoInteger(request.symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_EXECUTION_MARKET && (request.sl != class="num">0 || request.tp != class="num">0)) { result.retcode = TRADE_RETCODE_INVALID_STOPS; class="kw">return false; } result.balance = AccountBalance(); result.equity = AccountEquity(); result.profit = AccountEquity() - AccountBalance(); result.margin = margin; result.margin_free = AccountFreeMargin(); result.margin_level = class="num">0; result.comment = ""; class="kw">return true; } class="type">int PositionsTotal() { class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL) { count++; } } } class="kw">return count; } class="type">class="kw">string PositionGetSymbol(class="type">int index) { class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL) { if(index == count) { class="kw">return OrderSymbol(); } count++; } } } class="kw">return ""; } class="type">class="kw">ulong PositionGetTicket(class="type">int index) { class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL) { if(index == count) { class="kw">return OrderTicket(); } count++; } } } class="kw">return class="num">0; }
「用订单函数伪造持仓接口」
在 MT4 老账户或挂接历史订单数据时,MT5 原生的 PositionSelect 系列往往拿不到东西。上面这组函数直接把老式 Order 体系包装成 Position 语义,让后续逻辑无需重写就能跑。 PositionSelect 遍历 OrdersTotal 返回的所有订单,用 OrderSelect 按位置抓,再比对 symbol 且类型不超过 OP_SELL(即只认市价单),命中就返回 true。PositionSelectByTicket 更轻量,凭 ticket 直接 SELECT_BY_TICKET 校验类型,省掉循环。 Order2Position 是个小桥接:OP_BUY 映射 POSITION_TYPE_BUY,其余一律归为 SELL,注释里也点明「Position = order, 仅有 OP_BUY 或 OP_SELL」。 PositionGetInteger 和 PositionGetDouble 用 switch 把订单字段透传出来。注意 POSITION_TIME_MSC 直接拿 OrderOpenTime() 乘 1000,精度只到秒级毫秒化,不是真实微秒戳;POSITION_PRICE_CURRENT 则按多空分别取 MODE_BID / MODE_ASK,外汇与贵金属跳空时可能偏离上次成交价。 把这段代码贴进 MT5 的 MQL5 脚本里,用 EURUSD 实盘账户跑一遍 PositionSelect("EURUSD"),若返回 false 说明当前无市价单,可手动开 0.01 手验证接口联通性。杠杆市场高风险,验证仓位别超过日常千分之一。
class="type">bool PositionSelect(class="type">class="kw">string symbol) { for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderSymbol() == symbol && (OrderType() <= OP_SELL)) { class="kw">return true; } } } class="kw">return false; } class="type">bool PositionSelectByTicket(class="type">class="kw">ulong ticket) { if(OrderSelect((class="type">int)ticket, SELECT_BY_TICKET)) { if(OrderType() <= OP_SELL) { class="kw">return true; } } class="kw">return false; } class=class="str">"cmt">// Position = order, 仅有 OP_BUY 或 OP_SELL class="type">ENUM_POSITION_TYPE Order2Position(class="type">int type) { class="kw">return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; } class="type">bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, class="type">long &long_var) { class="kw">switch(property_id) { case POSITION_TICKET: case POSITION_IDENTIFIER: long_var = OrderTicket(); class="kw">return true; case POSITION_TIME: case POSITION_TIME_UPDATE: long_var = OrderOpenTime(); class="kw">return true; case POSITION_TIME_MSC: case POSITION_TIME_UPDATE_MSC: long_var = OrderOpenTime() * class="num">1000; class="kw">return true; case POSITION_TYPE: long_var = Order2Position(OrderType()); class="kw">return true; case POSITION_MAGIC: long_var = OrderMagicNumber(); class="kw">return true; } class="kw">return false; } class="type">bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, class="type">class="kw">double &double_var) { class="kw">switch(property_id) { case POSITION_VOLUME: double_var = OrderLots(); class="kw">return true; case POSITION_PRICE_OPEN: double_var = OrderOpenPrice(); class="kw">return true; case POSITION_SL: double_var = OrderStopLoss(); class="kw">return true; case POSITION_TP: double_var = OrderTakeProfit(); class="kw">return true; case POSITION_PRICE_CURRENT: double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK); class="kw">return true;
旧订单接口到持仓属性的桥接写法
把老式 OrderXxx 系列函数包一层,是 MT4 策略向 MT5 迁移时最常见的妥协方案。下面这段用 switch 把 POSITION_COMMISSION、POSITION_SWAP、POSITION_PROFIT 等枚举映射到 OrderCommission、OrderSwap、OrderProfit,调用方拿到 bool 返回值就能知道属性是否成功取出。 字符串类属性同理,POSITION_SYMBOL 走 OrderSymbol,POSITION_COMMENT 走 OrderComment,未命中枚举就返回 false。这种写法在 MT5 回测环境里能跑,但注意它依赖当前被 OrderSelect 选中的旧订单上下文,并非直接读持仓池。 PendingOrdersTotal 与 OrderGetTicket 都通过 OrderType() > OP_SELL 来筛挂单:OP_BUY 与 OP_SELL 是 0 和 1,大于 1 的才是 LIMIT / STOP 类待成交单。循环里每命中一个挂单 count 加一,OrderGetTicket 用 index == count 做随机访问,越界返回 0。 HistorySelect 自己维护 historyDeals 和 historyOrders 两个全局数组,按 from_date / to_date 过滤 OrdersHistoryTotal 里的记录。OrderType() <= OP_SELL 的判为成交(deal),其余进 orders 数组;时间条件用了 OpenTime >= from 或 CloseTime <= to,实际可能把边界外的单也收进来,接数据时需自己再核一遍。外汇与贵金属杠杆高,这类自写历史筛选若边界写错,统计胜率会偏得离谱。
case POSITION_COMMISSION: double_var = OrderCommission(); class="kw">return true; case POSITION_SWAP: double_var = OrderSwap(); class="kw">return true; case POSITION_PROFIT: double_var = OrderProfit(); class="kw">return true; } class="kw">return false; } class="type">bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, class="type">class="kw">string &string_var) { class="kw">switch(property_id) { case POSITION_SYMBOL: string_var = OrderSymbol(); class="kw">return true; case POSITION_COMMENT: string_var = OrderComment(); class="kw">return true; } class="kw">return false; } class="type">int PendingOrdersTotal() { class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() > OP_SELL) { count++; } } } class="kw">return count; } class="type">class="kw">ulong OrderGetTicket(class="type">int index) { class="type">int count = class="num">0; for(class="type">int i = class="num">0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() > OP_SELL) { if(index == count) { class="kw">return OrderTicket(); } count++; } } } class="kw">return class="num">0; } class="type">int historyDeals[], historyOrders[]; class="type">bool HistorySelect(class="type">class="kw">datetime from_date, class="type">class="kw">datetime to_date) { class="type">int deals = class="num">0, orders = class="num">0; ArrayResize(historyDeals, class="num">0); ArrayResize(historyOrders, class="num">0); for(class="type">int i = class="num">0; i < OrdersHistoryTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) { if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date) { if(OrderType() <= OP_SELL) class=class="str">"cmt">// deal { ArrayResize(historyDeals, deals + class="num">1); historyDeals[deals] = OrderTicket(); deals++; } ArrayResize(historyOrders, orders + class="num">1); historyOrders[orders] = OrderTicket(); orders++; }
◍ 历史订单与成交的索引映射技巧
在 MT5 的自定义回测环境里,历史成交和挂单往往共用同一套缓存数组,但对外暴露的 ticket 索引需要区分入场与离场。上面这组函数用 ArraySize(historyDeals)*2 把每笔订单拆成奇偶两个槽位:偶数槽对应原始 ticket,奇数槽则在高位打上 0x8000000000000000 标记,表示这是离场侧记录。 HistoryDealGetTicket 里 index%2==0 返回 OrderTicket(),否则返回带标记的值,这样调用层不用关心底层是开仓还是平仓。HistoryOrderGetTicket 对挂单只返一次(index%2==0),因为挂单没有对称的成交闭环,硬凑偶数槽会在日志里打印 '-> 0' 方便排查。 REVERSE(type) 宏用 (type+1)%2 做买卖反转,OrderType2DealType 把 MT4 的 OP_BUY/OP_SELL 映射到 DEAL_TYPE_BUY/SELL,中间留了 -1 占位给挂单类型。HistoryDealGetInteger 里用 exit 变量判断高位标记,决定取 OrderCloseTime 还是 OrderOpenTime——这套映射跑通后,你在 EA 里调 HistoryDealsTotal 会拿到双倍于真实成交笔数的索引量,属正常现象。 外汇与贵金属历史数据回测本身高风险,映射逻辑写错可能让策略误判平仓时间,建议直接把代码贴进 MT5 的 include 文件编译验证一次。
} } class="kw">return true; } class="type">int HistoryDealsTotal() { class="kw">return ArraySize(historyDeals) * class="num">2; } class="type">int HistoryOrdersTotal() { class="kw">return ArraySize(historyOrders) * class="num">2; } class="type">class="kw">ulong HistoryDealGetTicket(class="type">int index) { if(OrderSelect(historyDeals[index / class="num">2], SELECT_BY_TICKET, MODE_HISTORY)) { class=class="str">"cmt">// 奇数 - 入场 - 正数, 偶数 - 离场 - 负数 class="kw">return (index % class="num">2 == class="num">0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000); } class="kw">return class="num">0; } class="type">class="kw">ulong HistoryOrderGetTicket(class="type">int index) { if(OrderSelect(historyOrders[index / class="num">2], SELECT_BY_TICKET, MODE_HISTORY)) { if(OrderType() <= OP_SELL) { class="kw">return (index % class="num">2 == class="num">0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000); } else if(index % class="num">2 == class="num">0) class=class="str">"cmt">// 挂单返回一次 { class="kw">return OrderTicket(); } else { Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> class="num">0"); } } class="kw">return class="num">0; } class="type">bool HistoryDealSelect(class="type">class="kw">ulong ticket) { ticket &= ~0x8000000000000000; class="kw">return OrderSelect((class="type">int)ticket, SELECT_BY_TICKET, MODE_HISTORY); } class="macro">#define HistoryOrderSelect HistoryDealSelect class="macro">#define REVERSE(type) ((type + class="num">1) % class="num">2) ENUM_DEAL_TYPE OrderType2DealType(class="kw">const class="type">int type) { class="kw">static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -class="num">1, -class="num">1, -class="num">1, -class="num">1, DEAL_TYPE_BALANCE}; class="kw">return types[type]; } class="type">bool HistoryDealGetInteger(class="type">class="kw">ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, class="type">long &long_var) { class="type">bool exit = ((ticket_number & 0x8000000000000000) != class="num">0); ticket_number &= ~0x8000000000000000; if(OrderSelect((class="type">int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY)) { class="kw">switch(property_id) { case DEAL_TICKET: case DEAL_ORDER: case DEAL_POSITION_ID: long_var = OrderTicket(); class="kw">return true; case DEAL_TIME: long_var = exit ? OrderCloseTime() : OrderOpenTime(); class="kw">return true;