跨平台专家交易系统: 订单管理器·进阶篇
(2/3)· 从 MQL4 到 MQL5 的订单发送逻辑分叉,正悄悄制造重复开仓与手数错算
开平仓在 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_total | m_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_BUY | order.OrderType()==ORDER_TYPE_SELL) |
|---|
closed=m_trade.OrderClose((ulong)order.Ticket()); —— 市价单平仓 else closed=m_trade.OrderDelete((ulong)order.Ticket()); —— 挂单删除 外汇和贵金属杠杆高,回测通过不代表实盘能复现,开仓手数计算建议先在策略测试器用 0.01 手跑一遍。
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) { <span class="keyword">class="type">int</span> trades_total = TradesTotal(); <span class="keyword">class="type">int</span> orders_total = <span class="functions">OrdersTotal</span>(); m_symbol = m_symbol_man.Get(symbol); <span class="keyword">if</span> (!<span class="functions">CheckPointer</span>(m_symbol)) <span class="keyword">class="kw">return</span> <span class="macro">NULL</span>; <span class="keyword">if</span>(!IsPositionAllowed(type)) <span class="keyword">class="kw">return</span> <span class="macro">NULL</span>; <span class="keyword">if</span>(m_max_orders>orders_total && (m_max_trades>trades_total || m_max_trades<=<span class="number">class="num">0</span>)) { <span class="macro">ENUM_ORDER_TYPE</span> ordertype = type; <span class="keyword">class="type">class="kw">double</span> price=PriceCalculate(ordertype); <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>; <span class="keyword">if</span>(<span class="functions">CheckPointer</span>(m_main_stop)==<span class="macro">POINTER_DYNAMIC</span>) { sl = m_main_stop.StopLossCustom()?m_main_stop.StopLossCustom(symbol,type,price):m_main_stop.StopLossCalculate(symbol,type,price); tp = m_main_stop.TakeProfitCustom()?m_main_stop.TakeProfitCustom(symbol,type,price):m_main_stop.TakeProfitCalculate(symbol,type,price); } <span class="keyword">class="type">class="kw">double</span> lotsize=LotSizeCalculate(price,type,sl); <span class="keyword">class="type">class="kw">ulong</span> ticket = SendOrder(type,lotsize,price,sl,tp); <span class="keyword">if</span> (ticket><span class="number">class="num">0</span>) { <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>)) <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>()); } } <span class="keyword">class="kw">return</span> <span class="macro">NULL</span>; } <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>) { <span class="keyword">class="type">bool</span> closed=<span class="macro">true</span>; <span class="keyword">if</span>(<span class="functions">CheckPointer</span>(order)==<span class="macro">POINTER_DYNAMIC</span>) { <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>) m_symbol=m_symbol_man.Get(order.<span class="functions">Symbol</span>()); <span class="keyword">if</span>(<span class="functions">CheckPointer</span>(m_symbol)) m_trade=m_trade_man.Get(order.<span class="functions">Symbol</span>()); <span class="keyword">if</span>(order.<span class="predefines">Volume</span>()><span class="number">class="num">0</span>) { <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>) closed=m_trade.<span class="functions">OrderClose</span>((<span class="keyword">class="type">class="kw">ulong</span>)order.Ticket()); <span class="keyword">else</span> 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 验证。
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 失败才走平仓分支,若挂单已部分成交,删除失败但不会触发反向单,实盘里要自己补一层状态检查。
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 五分钟图回测,能直接看到平仓分支的命中次数。 外汇与贵金属杠杆高,这类退出函数若漏掉对冲单或挂单状态判断,可能误平或漏平,实盘前务必在策略测试器用至少三个月数据验证。
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,调用方必须判空。
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。 做外汇、贵金属的要注意,这种删建切换在快速波动行情里可能带来滑点和小概率重复发单,实盘前务必在策略测试器里跑一遍撤单重建延迟。