跨平台专家交易系统: 订单管理器·进阶篇
🔀

跨平台专家交易系统: 订单管理器·进阶篇

(2/3)· 从 MQL4 到 MQL5 的订单发送逻辑分叉,正悄悄制造重复开仓与手数错算

实战向进阶 第 2/3 篇
把 MT4 的 OrderSend 直接照搬到 MT5,编译能过但下单必乱。两平台交易请求结构和成交回执处理完全不同,硬套只会在实盘里开出双倍仓位或止损失效。

开平仓在 MQL4 兼容层怎么落地

做 MT4/MT5 双平台 EA 时,最麻烦的是订单模型不一致。MQL4 只有对冲一种保证金模式,所以开仓和平仓逻辑比 MQL5 干净得多:挂单删掉、市价单反向平仓,流程不随经纪商禁不禁对冲而改变。 TradeOpen 方法先抓两个计数——TradesTotal 和 OrdersTotal,再拿品种对象。只有同时满足最大订单数与最大交易数两个预设,才去算止损、止盈和手数,最后发单并把新 COrder 塞进活动列表。 CloseOrder 接收订单指针和可选索引。索引已知能省掉一次数组查找;未知时方法内部调 FindOrderIndex 定位。平仓后对象先从活动列表移除、压到历史列表尾端,标记关闭且交易量清零。 下面这段是核心实现的原文,逐行拆一下关键动作: COrder* COrderManager::TradeOpen(const string symbol,ENUM_ORDER_TYPE type) —— 开仓入口,传品种名和订单类型 int trades_total = TradesTotal(); —— 取当前持仓(交易)总数 int orders_total = OrdersTotal(); —— 取当前订单总数(含挂单) m_symbol = m_symbol_man.Get(symbol); —— 从管理器拿品种信息对象 if (!CheckPointer(m_symbol)) return NULL; —— 指针无效直接退出 if(!IsPositionAllowed(type)) return NULL; —— 该类型不被允许也退出

if(m_max_orders>orders_total && (m_max_trades>trades_totalm_max_trades<=0)) —— 订单数未超上限,且交易数未超上限(或上限设为0表示不限制)

ENUM_ORDER_TYPE ordertype = type; —— 类型本地化 double price=PriceCalculate(ordertype); —— 按类型算挂单价或市价参考 double sl=0,tp=0; —— 初始化止损止盈 if(CheckPointer(m_main_stop)==POINTER_DYNAMIC) —— 止损止盈对象存在才计算 sl = m_main_stop.StopLossCustom()?...:m_main_stop.StopLossCalculate(...); —— 有自定义用自定义,否则用计算值 tp = m_main_stop.TakeProfitCustom()?...:m_main_stop.TakeProfitCalculate(...); —— 止盈同理 double lotsize=LotSizeCalculate(price,type,sl); —— 根据价格、类型、止损算手数 ulong ticket = SendOrder(type,lotsize,price,sl,tp); —— 发单拿ticket if (ticket>0) —— 发单成功 if (OrderSelect((int)ticket,SELECT_BY_TICKET)) —— 选中订单 return m_orders.NewOrder(...); —— 新建COrder并返回指针 return NULL; —— 任何环节失败返回空 bool COrderManager::CloseOrder(COrder *order,const int index=-1) —— 平仓入口,index默认-1表示未知 bool closed=true; —— 默认成功 if(CheckPointer(order)==POINTER_DYNAMIC) —— 订单指针有效

if(!CheckPointer(m_symbol)StringCompare(m_symbol.Name(),order.Symbol())!=0)

m_symbol=m_symbol_man.Get(order.Symbol()); —— 品种对象不对就重取 if(CheckPointer(m_symbol)) m_trade=m_trade_man.Get(order.Symbol()); —— 取交易对象 if(order.Volume()>0) —— 还有量才操作

if(order.OrderType()==ORDER_TYPE_BUYorder.OrderType()==ORDER_TYPE_SELL)

closed=m_trade.OrderClose((ulong)order.Ticket()); —— 市价单平仓 else closed=m_trade.OrderDelete((ulong)order.Ticket()); —— 挂单删除 外汇和贵金属杠杆高,回测通过不代表实盘能复现,开仓手数计算建议先在策略测试器用 0.01 手跑一遍。

MQL5 / C++
COrder* COrderManager::TradeOpen(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> symbol,<span class="macro">ENUM_ORDER_TYPE</span> type)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">int</span> trades_total = TradesTotal();
&nbsp;&nbsp; <span class="keyword">class="type">int</span> orders_total = <span class="functions">OrdersTotal</span>();
&nbsp;&nbsp; m_symbol = m_symbol_man.Get(symbol);
&nbsp;&nbsp; <span class="keyword">if</span> (!<span class="functions">CheckPointer</span>(m_symbol))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">NULL</span>;
&nbsp;&nbsp; <span class="keyword">if</span>(!IsPositionAllowed(type))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">NULL</span>;
&nbsp;&nbsp; <span class="keyword">if</span>(m_max_orders&gt;orders_total &amp;&amp; (m_max_trades&gt;trades_total || m_max_trades&lt;=<span class="number">class="num">0</span>))
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="macro">ENUM_ORDER_TYPE</span> ordertype = type;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> price=PriceCalculate(ordertype);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> sl=<span class="number">class="num">0</span>,tp=<span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">CheckPointer</span>(m_main_stop)==<span class="macro">POINTER_DYNAMIC</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> lotsize=LotSizeCalculate(price,type,sl);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">ulong</span> ticket = SendOrder(type,lotsize,price,sl,tp);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (ticket&gt;<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> (<span class="functions">OrderSelect</span>((<span class="keyword">class="type">int</span>)ticket,<span class="macro">SELECT_BY_TICKET</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> m_orders.NewOrder(<span class="functions">OrderTicket</span>(),<span class="functions">OrderSymbol</span>(),<span class="functions">OrderMagicNumber</span>(),(<span class="macro">ENUM_ORDER_TYPE</span>)::<span class="functions">OrderType</span>(),::<span class="functions">OrderLots</span>(),::<span class="functions">OrderOpenPrice</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">class="kw">return</span> <span class="macro">NULL</span>;
&nbsp;&nbsp;}
<span class="keyword">class="type">bool</span> COrderManager::CloseOrder(COrder *order,<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> index=-<span class="number">class="num">1</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">bool</span> closed=<span class="macro">true</span>;
&nbsp;&nbsp; <span class="keyword">if</span>(<span class="functions">CheckPointer</span>(order)==<span class="macro">POINTER_DYNAMIC</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(!<span class="functions">CheckPointer</span>(m_symbol) || <span class="functions">StringCompare</span>(m_symbol.Name(),order.<span class="functions">Symbol</span>())!=<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_symbol=m_symbol_man.Get(order.<span class="functions">Symbol</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">CheckPointer</span>(m_symbol))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_trade=m_trade_man.Get(order.<span class="functions">Symbol</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(order.<span class="predefines">Volume</span>()&gt;<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(order.<span class="functions">OrderType</span>()==<span class="macro">ORDER_TYPE_BUY</span> || order.<span class="functions">OrderType</span>()==<span class="macro">ORDER_TYPE_SELL</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;closed=m_trade.<span class="functions">OrderClose</span>((<span class="keyword">class="type">class="kw">ulong</span>)order.Ticket());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;closed=m_trade.<span class="functions">OrderDelete</span>((<span class="keyword">class="type">class="kw">ulong</span>)order.Ticket());

「成交单闭环的归档与 detach 处理」

这段逻辑出现在订单管理类的 Close 流程末尾,负责把已平仓对象从活跃队列里摘掉并转入归档。先通过 index 是否大于等于 0 来决定用传入下标还是现查 FindOrderIndex,避免重复查找。 若 ArchiveOrder 对 Detach 出来的指针归档成功,就顺手把 order 的 Close 置位、Volume 清 0,防止后续逻辑误读成还有仓。 返回 closed 标志让上层知道这次平仓动作是否真的走完了。实盘里若漏掉 Detach,m_orders 数组会残留空壳订单,MT5 策略测试器跑长周期回测时内存占用可能涨 20%~30%,值得在 OnInit 后打印一次 Size 验证。

MQL5 / C++
   if(closed)
      {
       class="type">int idx = index>=class="num">0?index:FindOrderIndex(GetPointer(order));
       if(ArchiveOrder(m_orders.Detach(idx)))
         {
          order.Close();
          order.Volume(class="num">0);
         }
      }
   class="kw">return closed;

◍ 开平逻辑在 MT5 里的两套写法

MT5 下封装开仓时,最直观的变化是不必像 MQL4 那样显式抓取止损止盈价位。原因在 MetaTrader 5 的停止级别机制不同于前代,这套库倾向于用挂单去对标 MQL4 里经纪商侧的 stop 层级,开仓函数里传参直接给 0 也能跑通。 看 TradeOpen 的实现:先由 IsPositionAllowed 卡掉不允许的订单类型,再用 m_max_orders 与 OrdersTotal()、m_max_trades 与 TradesTotal() 做数量闸门。lotsize 通过 LotSizeCalculate 算出来,其中若 m_main_stop 非空会代入 StopLossCalculate 的返回值,但 SendOrder 调用时止损止盈位仍传 0。 平仓侧必须先弄清手上是订单还是持仓。MQL5 里一笔订单常是交易请求落地的产物,可能对应一组成交,也可能只是未即时执行的挂单;用 COrderInfo::Select 能快速判别,选不上就归为市价单或持仓。 对冲账户直接 PositionClose 按 ticket 平掉;净持账户则反向手数等价成交来冲抵。外汇与贵金属保证金波动剧烈,净持对冲切换错模式可能让仓位对冲失效,上 MT5 前先确认账户类型。 别把正态当圣经:这段 CloseOrder 里 ord.Select 失败才走平仓分支,若挂单已部分成交,删除失败但不会触发反向单,实盘里要自己补一层状态检查。

MQL5 / C++
COrder* COrderManager::TradeOpen(class="kw">const class="type">class="kw">string symbol,ENUM_ORDER_TYPE type)
  {
   class="type">class="kw">double lotsize=class="num">0.0,price=class="num">0.0;
   class="type">int trades_total =TradesTotal();
   class="type">int orders_total = OrdersTotal();
   m_symbol=m_symbol_man.Get(symbol);
   if(!IsPositionAllowed(type))
      class="kw">return NULL;
   if(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=class="num">0))
     {
       price=PriceCalculate(type);
       lotsize=LotSizeCalculate(price,type,m_main_stop==NULL?class="num">0:m_main_stop.StopLossCalculate(symbol,type,price));
       if (SendOrder(type,lotsize,price,class="num">0,class="num">0))
         class="kw">return m_orders.NewOrder((class="type">int)m_trade.ResultOrder(),m_trade.RequestSymbol(), (class="type">int)m_trade.RequestMagic(),m_trade.RequestType(),m_trade.ResultVolume(),m_trade.ResultPrice());
     }       
   class="kw">return NULL;
  }
class="type">bool COrderManager::CloseOrder(COrder *order,class="kw">const class="type">int index=-class="num">1)
  {
   class="type">bool closed=true;
   COrderInfo ord;
   if(!CheckPointer(order))
      class="kw">return true;
   if(order.Volume()<=class="num">0)
      class="kw">return true;
   if(!CheckPointer(m_symbol) || StringCompare(m_symbol.Name(),order.Symbol())!=class="num">0)
      m_symbol=m_symbol_man.Get(order.Symbol());
   if(CheckPointer(m_symbol))
      m_trade=m_trade_man.Get(order.Symbol());
   if(ord.Select(order.Ticket()))
   {
      closed=m_trade.OrderDelete(order.Ticket());
   } 
   else
    {
       ResetLastError();
       if(IsHedging())
       {
         closed=m_trade.PositionClose(order.Ticket());
       } 
       else
         {
           if(COrder::IsOrderTypeLong(order.OrderType()))
              closed=m_trade.Sell(order.Volume(),class="num">0,class="num">0,class="num">0);
           else if(COrder::IsOrderTypeShort(order.OrderType()))
              closed=m_trade.Buy(order.Volume(),class="num">0,class="num">0,class="num">0);
         }
    }
   if(closed)
     {
       if(ArchiveOrder(m_orders.Detach(index)))
         {
           order.Close();
           order.Volume(class="num">0);
         }
     }

持仓平仓判定的代码落点

MQL5 里把已平仓状态直接回传,是很多 EA 退出逻辑里最薄的一层。上面这段 return closed 出现在某个持仓检查函数的末尾,意味着只要前面把 closed 置真,函数就交还控制权给调用方。 读这段代码要注意:closed 的真假不是在 return 这一行决定的,而是前面遍历持仓、比对平仓条件时赋的值。你开 MT5 把这段塞进自己的 PositionSelect 循环里,跑 EURUSD 五分钟图回测,能直接看到平仓分支的命中次数。 外汇与贵金属杠杆高,这类退出函数若漏掉对冲单或挂单状态判断,可能误平或漏平,实盘前务必在策略测试器用至少三个月数据验证。

MQL5 / C++
  class="kw">return closed;
}

「COrder 实例在 NewOrder 里的诞生路径」

订单管理器不会在 NewOrder 里直接写止损止盈,那部分逻辑被推到了 COrder 的 Init 阶段。两个平台对止损止盈价位的实现差异,目前在这一层还没统一处理,所以 Init 接收的第二个参数很关键。 它拿到的是一个 CStops 容器指针,里面装着具体的停止对象(例如前文提到的 m_main_stop)。这个容器类本身不在本节展开,留到后续单独讲。 下面这段 NewOrder 代码是 COrdersBase 里的实际写法,跑 MT5 时你可以在 COrdersBase 派生类里直接断点验证: COrder* COrdersBase::NewOrder 先 new 出一个 COrder 动态对象,用 CheckPointer 确认是 POINTER_DYNAMIC 后才进插入排序。InsertSort 成功才补 magic 并调 Init,把订单管理器自身指针和 m_stops 传进去;任何一步失败都返回 NULL,调用方必须判空。

MQL5 / C++
COrder* 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);
         order.Init(GetPointer(this),m_stops);
         class="kw">return order;
      }  
   class="kw">return NULL;
    }

◍ MT5 改挂单只能删了重下

订单管理器到现在还没碰过修改现有持仓的逻辑,这部分交给 CStop 和 CorderStop 两个停止对象接管,它们负责更新止损位、协调所属 COrder 的状态,具体留到别的篇幅拆。 MT4 里挂单入场价能反复改,MT5 直接砍了这块自由。MQL5 属于受限模型,标准做法不是 ModifyOrder,而是先 DeleteOrder 再带新属性 ReCreate。 做外汇、贵金属的要注意,这种删建切换在快速波动行情里可能带来滑点和小概率重复发单,实盘前务必在策略测试器里跑一遍撤单重建延迟。

把跨平台校验交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 在 MT4/MT5 下的订单请求差异告警,你专注策略本身而非接口坑。

常见问题

具体算法随策略变化太大,基类只定义调用契约,把实现委托给成员对象,避免每换策略就改管理器核心。
MT5 把请求和结果拆成两个 struct,发送后必须读结果结构确认成交,忽略回执就会以为下单失败而重复发单。
这两个是防重复交易的硬约束,放管理器内最稳;信号和时间限制依赖策略上下文,放外部组件更灵活。
可以,小布盯盘会解析 EA 交易动作来源,标出当前是走 MQL4 兼容层还是原生 MQL5 实现,方便你排查跨平台行为。
会,魔幻数字是跨会话区分订单归属的主键,写错会导致管理器计数活跃仓位数失真,进而突破预设上限。