通用智能交易系统:支持挂单和对冲(第五章)·进阶篇
⚙️

通用智能交易系统:支持挂单和对冲(第五章)·进阶篇

(2/3)· 从价格访问重写到CPendingOrders,把复杂挂单逻辑塞进可复用的CStrategy骨架

实战向 第 2/3 篇
很多EA作者在切换MT5对冲账户时直接沿用净仓逻辑,结果订单方向错乱却查不出原因。挂单处理若还靠手写SymbolInfoDouble加精度修正,既易漏边界又拖慢迭代。这一篇把价格访问、挂单与对冲一次性收进交易引擎。

持仓利润与激活状态的底层取法

在 MT5 的持仓封装类里,取浮动利润前先判活是个容易被忽略的细节。CPosition::Profit 直接走 PositionGetDouble(POSITION_PROFIT),但若持仓未激活会返回脏值,所以先用 IsActive 拦一道,非激活就给 0.0。 IsActive 的写法区分了账户保证金模式。当模式不是 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 时,用 PositionSelect(m_symbol) 按交易品种挑当前持仓;而对冲账户里同品种可有多仓,必须 PositionSelectByTicket(m_id) 用持仓票据精确锁定。 开 MT5 按 F4 把这段塞进自己的 EA 头文件,挂一个 EURUSD 多单后改 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 的返回逻辑做断点,能看到非对冲模式下 m_symbol 选错仓时 Profit 仍可能读出非零值——这是品种级选中而非票据级选中的副作用。外汇与贵金属杠杆高,持仓状态误判会放大平仓逻辑错误,验证时请用模拟账户。

MQL5 / C++
class="type">class="kw">double CPosition::Profit(class="type">void)
  {
   if(!IsActive())
     class="kw">return class="num">0.0;
   class="kw">return PositionGetDouble(POSITION_PROFIT);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回true如果头寸被激活。否则返回false     
class=class="str">"cmt">//||
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPosition::IsActive(class="type">void)
{
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
     class="kw">return PositionSelect(m_symbol);
   else
     class="kw">return PositionSelectByTicket(m_id);
}

「老版引擎里手动挂 BuyStop 的活法」

早期 CStrategy 交易引擎原生不支持挂单,但不代表基于它写的 EA 不能下挂单。直接用 MT5 标准函数 OrdersTotal() 和 OrderSelect() 就能绕开限制,把挂单逻辑塞进 BuyInit / SellInit 重写方法里。 一个典型脉冲思路是:每个新柱形把 BuyStop 的激活价钉在当前 Ask 上方 0.25%,若价格在本柱内出现突发行情就被触发,赶上强动量阶段进场;若波动不够,柱终前把挂单改到新价位,等下一根柱重新试。 下面这段 CMovingAverage::InitBuy 就是第一版里没靠类原生支持、纯手写的买向挂单处理。它先卡事件与已有买单,再扫一遍挂单归属本 EA 的 BuyStop 并改价,一个都没有才新下一张。 挂单操作和普通订单本质一样,核心纪律只有一条:买卖逻辑必须在 BuyInit 和 SellInit 里彻底分开,别把 sell 类挂单混进买方法。剩下遍历、识别、改单的写法,和 MT4 经典订单循环没大区别。外汇与贵金属杠杆高,这类突发行情触发概率随品种波动环境变化,实盘前请在 MT5 策略测试器用真实点差验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Buy类型挂单的执行逻辑    
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMovingAverage::InitBuy(class="kw">const MarketEvent &event)
  {
   if(!IsTrackEvents(event))class="kw">return;                                      class=class="str">"cmt">// 仅处理所需事件
   if(positions.open_buy > class="num">0) class="kw">return;                                    class=class="str">"cmt">// 如果已经存在至少一个未平仓的买单,无需再买入
   class="type">int buy_stop_total = class="num">0;
   for(class="type">int i = OrdersTotal()-class="num">1; i >= class="num">0; i--)
   {
      class="type">class="kw">ulong ticket = OrderGetTicket(i);
      if(!OrderSelect(ticket))class="kw">continue;
      class="type">class="kw">ulong magic = OrderGetInteger(ORDER_MAGIC);
      if(magic != ExpertMagic())class="kw">continue;
      class="type">class="kw">string symbol = OrderGetString(ORDER_SYMBOL);
      if(symbol != ExpertSymbol())class="kw">continue;
      ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      if(order_type == ORDER_TYPE_BUY_STOP)
      {
         buy_stop_total++;
         Trade.OrderModify(ticket, Ask()*class="num">0.0025, class="num">0, class="num">0, class="num">0);
      }
   }
   if(buy_stop_total == class="num">0)
      Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*class="num">0.0025, ExpertSymbol(), class="num">0, class="num">0, NULL);
  }

◍ 用字典容器管住挂单对象的生命周期

在 CStrategy 这套面向对象框架里,挂单不是靠系统函数随手查,而是被封装成 CPendingOrder 对象。麻烦在于:真实挂单被删了,内存里的对象若还活着,EA 会误以为单子还在,进而下出重复或反向的错单。外汇和贵金属市场滑点快、删单频繁,这种不同步在高波动时段尤其容易引发连错。 COrdersEnvironment 就是专门兜住这层同步风险的。它只暴露两个入口:Total 直接回传 OrdersTotal() 的系统值,不会自己算错;GetOrder 按索引取单,内部先调 OrderGetTicket,拿不到合法 ticket(返回 0)就直接给 NULL,从根上杜绝访问已消失的挂单。 对象创建走的是「按需 new + 字典缓存」的路子。m_orders 这个 CDictionary 以 ticket 为唯一键,第一次碰某个 ID 才 new 一个 CPendingOrder 并塞进容器,之后同 ID 直接返旧引用。好处很实:一是避免重复构造浪费资源,二是用户永远不用手动 delete,容器随环境对象析构一起清掉。 下面这段是环境类的核心源码,注意 GetOrder 里 ContainsKey 的判断顺序——先查缓存、再 OrderSelect 校验,逻辑反了就会每次重建。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于操作挂单的一个类
class=class="str">"cmt">//+------------------------------------------------------------------+
class COrdersEnvironment
{
class="kw">private:
  CDictionary      m_orders;           class=class="str">"cmt">// 挂单的总数
class="kw">public:
                  COrdersEnvironment(class="type">void);
  class="type">int             Total(class="type">void);
  CPendingOrder* GetOrder(class="type">int index);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 我们需要知道当前货币对及EA的编号
class=class="str">"cmt">//+------------------------------------------------------------------+
COrdersEnvironment::COrdersEnvironment(class="type">void)
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回一个挂单
class=class="str">"cmt">//+------------------------------------------------------------------+
CPendingOrder* COrdersEnvironment::GetOrder(class="type">int index)
{
  class="type">class="kw">ulong ticket = OrderGetTicket(index);
  if(ticket == class="num">0)
    class="kw">return NULL;
  if(!m_orders.ContainsKey(ticket))
    class="kw">return m_orders.GetObjectByKey(ticket);
  if(OrderSelect(ticket))
    class="kw">return NULL;
  CPendingOrder* order = new CPendingOrder(ticket);
  m_orders.AddObject(ticket, order);
  class="kw">return order;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回挂单的数量
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int COrdersEnvironment::Total(class="type">void)
{
  class="kw">return OrdersTotal();  
}

EA里挂单的接管与改价逻辑

在重写挂单执行逻辑时,PendingOrders 容器本质是一个 COrdersEnvironment 实例,搜索方式与系统函数一致:按索引 i 取 CPendingOrder 对象,取不到或订单不属于本 EA 就跳过。判断归属用 IsMain,比对订单的货币对与 magic 编号,一致才视为自家挂单。 若挂单类型为 ORDER_TYPE_BUY_STOP 且未激活,代码将其修正位置设为当前 Ask 加 0.25%(即 Ask() + Ask()*0.0025)。修改动作走 Modify 重载版,可顺带改其他参数。注意一个反直觉点:操作完挂单后,不要手动删订单对象,PendingOrders 自己管 CPendingOrder 生命周期,用户函数里留着引用即可。 当循环跑完 buy_stop_total 还是 0,说明没有任何自家 buy stop,这时才用 Trade.BuyStop 丢一个新挂单,挂单价同样偏移 0.25%。外汇与贵金属杠杆高,这类偏移阈值在跳空行情可能直接不成交或滑点放大,建议在 MT5 策略测试器用真实点差回放验证。 下面这段是 InitBuy 里挂单处理的骨架,重点看循环内的归属判断与改价,以及末尾的计数开单:

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 当快速MA在慢速MA之下卖出                                               </span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CMovingAverage::InitBuy(<span class="keyword">class="kw">const</span> MarketEvent &amp;event)
  {
   <span class="keyword">if</span>(!IsTrackEvents(event))<span class="keyword">class="kw">return</span>;                                        class=class="str">"cmt">// 仅处理所需事件
   <span class="keyword">if</span>(positions.open_buy &gt; <span class="number">class="num">0</span>) <span class="keyword">class="kw">return</span>;                                        class=class="str">"cmt">// 如果已经存在至少一个未平仓的买单,无需再买入
   <span class="keyword">class="type">int</span> buy_stop_total = <span class="number">class="num">0</span>;
   <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = PendingOrders.Total()-<span class="number">class="num">1</span>; i &gt;= <span class="number">class="num">0</span>; i--)
     {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      <span class="keyword">if</span>(Order == <span class="macro">NULL</span> || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         <span class="keyword">class="kw">continue</span>;
      <span class="keyword">if</span>(Order.Type() == <span class="macro">ORDER_TYPE_BUY_STOP</span>)
        {
         buy_stop_total++;
         Order.Modify(Ask() + Ask()*<span class="number">class="num">0.0025</span>);
        }
      <span style="background-class="type">color:rgb(class="num">255, class="num">246, class="num">200);"><span class="stroke">class=class="str">"cmt">//删除订单;</span><span style="background-class="type">color:rgb(class="num">255, class="num">246, class="num">200);">没有必要删除订单对象!</span>
</span>     }
   <span class="keyword">if</span>(buy_stop_total == <span class="number">class="num">0</span>)
      Trade.BuyStop(MM.GetLotFixed(), Ask() + Ask()*<span class="number">class="num">0.0025</span>, ExpertSymbol(), <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="macro">NULL</span>);
  }

「CImpulse 挂单触发与均线平仓机制」

CImpulse 策略在每个新柱开盘时,按开盘价上下各 0.1% 的距离同时挂出 BuyStop 与 SellStop。若某一单在该柱内被触及,即视为价格出现剧烈单向运动,挂单转持仓。 持仓管理只用一条简单移动平均线:价格回踩到均线即平仓。实测图中多头情景里,黑色三角标出柱起点,0.1% 外挂单之一触发后开多,待某根 K 线收在红色均线下方,EA 以蓝色三角标记平掉该多单。 一个易坑点:BuyStop 挂价可能低于均线,触发后价格已在均线下方,会被立刻反手平掉。原版逻辑在 BuyInit / SellInit 里补了一道校验——仅当市价高于均线才挂 BuyStop,低于均线才挂 SellStop,避开秒平。 MT5 对冲账户可同柱多空并存,但本策略多空独立清算:柱开价高于均线清空、价跌穿均线清多,与是否对冲账户无关。开测前需在开户界面勾选对冲选项。

◍ CImpulse类的轻量挂单实现

新EA的核心是一个名为CImpulse的策略类,它被刻意做得极简:不挂日志函数,也不从XML解析参数,目的只是把挂单逻辑讲清楚。类里只保留一个m_percent字段用来描述挂单相对价格的百分比偏移,以及InitBuy/InitSell等几个虚函数钩子。 配置入口在mq5主文件,外部参数StopPercent默认0.05,即挂单触发价距离参考价约5%以内。EA通过#include引入Strategy和MovingAverage两个基础模块,枚举ORDER_TASK_DELETE与ORDER_TASK_MODIFY区分删单与改单两种任务。 InitBuy函数展示了实战中的过滤:先由IsTrackEvents判断是否追踪该事件,若当前已有open_buy持仓(positions.open_buy>0)直接退出,避免重复堆单。外汇与贵金属杠杆高,这类过滤能显著降低非理性加仓概率。 把下面代码直接丢进MT5的MQL5/Include/Strategy目录编译,可验证挂单类骨架是否跑通;调StopPercent看BuyStop距离变化最直观。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Impulse.mqh |
class=class="str">"cmt">//|                                     Copyright class="num">2016, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Strategy\Strategy.mqh>
class="macro">#include <Strategy\Indicators\MovingAverage.mqh>
input class="type">class="kw">double StopPercent = class="num">0.05;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定义挂单相关操作
class=class="str">"cmt">//| 
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_ORDER_TASK
{
   ORDER_TASK_DELETE,   class=class="str">"cmt">// 删除挂单
   ORDER_TASK_MODIFY    class=class="str">"cmt">// 修改挂单
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CImpulse策略        
class=class="str">"cmt">//+------------------------------------------------------------------+
class CImpulse : class="kw">public CStrategy
{
class="kw">private:
   class="type">class="kw">double            m_percent;            class=class="str">"cmt">// 挂单位置的百分比值
   class="type">bool              IsTrackEvents(class="kw">const MarketEvent &event);
class="kw">protected:
   class="kw">virtual class="type">void      InitBuy(class="kw">const MarketEvent &event);
   class="kw">virtual class="type">void      InitSell(class="kw">const MarketEvent &event);
   class="kw">virtual class="type">void      SupportBuy(class="kw">const MarketEvent &event,CPosition *pos);
   class="kw">virtual class="type">void      SupportSell(class="kw">const MarketEvent &event,CPosition *pos);
   class="kw">virtual class="type">void      OnSymbolChanged(class="type">class="kw">string new_symbol);
   class="kw">virtual class="type">void      OnTimeframeChanged(ENUM_TIMEFRAMES new_tf);
class="kw">public:
   class="type">class="kw">double            GetPercent(class="type">void);
   class="type">void              SetPercent(class="type">class="kw">double percent);
   CIndMovingAverage Moving;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 建立一个BuyStop多头挂单
class=class="str">"cmt">//| 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CImpulse::InitBuy(class="kw">const MarketEvent &event)
{
   if(!IsTrackEvents(event))class="kw">return;                       
   if(positions.open_buy > class="num">0) class="kw">return;                     
   class="type">int buy_stop_total = class="num">0;
   ENUM_ORDER_TASK task;
把挂单回测交给小布盯盘
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到策略在对冲与净仓两种账户下的订单流差异,你只需核对逻辑分支。

常见问题

旧写法每次取Ask/Bid/Last都要手动按点数精度舍入,分散在策略里容易不一致;重写后引擎内部统一处理,调用方只拿干净价格。
核心改动很小,只在订单环境层适配双边持仓,原有策略类基本不动,说明初始抽象是对的。
前者封装挂单的提交与状态追踪,后者提供订单执行的上下文环境,两者配合把挂单从裸API调用变成可组合模块。
可以,小布盯盘的品种页支持读取EA订单流并标注挂单位置,省去你在MT5里逐单翻记录。
能但很麻烦,没有专用封装,作者要自己拼请求与校验;本篇才把挂单变成一等公民。
重点看挂单触发后仓位计数方式,对冲账户会出现同符号多仓,净仓账户则是加减仓,回测样本要覆盖两类。