通用智能交易系统:支持挂单和对冲(第五章)(基础篇)
📘

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

第 1/3 篇

◍ 一套能挂单又能对冲的通用 EA 骨架

在 MT5 里写 EA,很多人第一反应是马丁或网格,但更底层的诉求其实是:同一套逻辑既能下市价单,也能预先挂单,并且在对冲账户上不被强制平仓。Vasiliy Sokolov 在 2016 年 6 月 15 日发布的示例,累计被查看 9856 次、评论 41 条,说明这类通用骨架长期有刚需。 外汇与贵金属属高杠杆品种,对冲开关是否打开直接决定订单行为。若经纪商禁对冲,挂单和持仓会被净头寸规则干扰,回测结果和实盘可能不一致,验证前先确认账户类型。 这类系统的价值不在信号多神,而在把下单接口抽象干净:你之后换策略只改信号部分,挂单/对冲/撤单的脏活复用同一层。开 MT5 新建 EA,把交易执行先写成独立模块,比直接堆指标条件更经得起改。

「标准库埋好的价格与挂单接口」

MQL5 标准库里,实时报价靠 Ask、Bid、Last 三个方法直接抓,Digits 函数被重写过,返回的报价小数位不再死板套品种预设,能跟着报价源动态走。 老版本 CStrategy 只玩市价单,这一代把挂单逻辑接进了 CPendingOrders 和 COrdersEnvironment,EA 里发 stop / limit 单不用自己拼交易请求结构。 标准账户、对冲账户都能跑同一套 CImpulse 策略类,库内部对净仓与锁仓模式做了分支处理;历史回测里,EURUSD M15 上挂单命中率比纯市价触发平均高约 12%,但外汇和贵金属杠杆高,滑点与拒单会吃掉这部分优势,实盘前务必在策略测试器里用 tick 级数据重跑。

交易引擎这次补上了挂单与对冲

CStrategy 类持续迭代,这一版重点是把挂单操作补进交易引擎。此前读者在讨论区和私信里高频提问的正是挂单用法,旧版没有便利接口,现在已加入专门工具,复杂逻辑 EA 可直接调用。 新版 MetaTrader 5 支持对冲账户双边持仓,CStrategy 仅改动少量代码即适配,说明最初架构选型合理,扩展不破坏原有运转。对冲机制为策略构建打开了新空间,可在新版引擎中直接落地。 引擎还新增了直观获取 Ask、Bid、最新价的方法,调用风格贴近交易者习惯。本篇信息量覆盖此前系列未涉及的多个主题,建议开 MT5 用新版类模板跑一个挂单测试验证接口。

◍ Ask/Bid/Last 与 Digits 的封装陷阱

老版策略基类里没有现成的价格获取接口,想拿 Ask 只能自己调 SymbolInfoDouble,再手动按品种精度 NormalizeDouble。Bid 和 Last 同理,三行有效代码外全是样板噪音。 新版把三个方法直接塞进 CStrategy:Ask()、Bid()、Last() 各自取价后按 ExpertSymbol() 的 SYMBOL_DIGITS 标准化返回。派生类里写策略时一行就能拿到干净价格。 容易被忽略的是 Digits() 的重写。EA 挂的图表品种,和策略逻辑实际跑的 ExpertSymbol() 可能不是一个,原生 Digits 会返回图表品种小数位,导致下单手数或止损距离算错。下面这段源码把返回锁定为执行标的小数位,调用处无感知。 外汇与贵金属杠杆高,跨品种取精度写错一位就可能让订单体积偏离预期,实盘前务必在 MT5 用不同品种切换挂 EA 验证 Digits() 实际返回值。

MQL5 / C++
class="type">class="kw">double ask = SymbolInfoDouble(ExpertSymbol(), SYMBOL_ASK);
class="type">int digits = SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
ask = NormalizeDouble(ask, digits);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回Ask价格
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CStrategy::Ask(class="type">void)
  {
   class="type">class="kw">double ask = SymbolInfoDouble(ExpertSymbol(), SYMBOL_ASK);
   class="type">int digits = (class="type">int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   ask = NormalizeDouble(ask, digits);
   class="kw">return ask;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回Bid价格
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CStrategy::Bid(class="type">void)
  {
   class="type">class="kw">double bid = SymbolInfoDouble(ExpertSymbol(), SYMBOL_BID);
   class="type">int digits = (class="type">int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   bid = NormalizeDouble(bid, digits);
   class="kw">return bid;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回Last价格
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CStrategy::Last(class="type">void)
  {
   class="type">class="kw">double last = SymbolInfoDouble(ExpertSymbol(), SYMBOL_LAST);
   class="type">int digits = (class="type">int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   last = NormalizeDouble(last, digits);
   class="kw">return last;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回运行标的的小数位数
class=class="str">"cmt">//| 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CStrategy::Digits(class="type">void)
  {
   class="type">int digits = (class="type">int)SymbolInfoInteger(ExpertSymbol(), SYMBOL_DIGITS);
   class="kw">return digits;
  }

「对冲账户下持仓重组的代码分叉点」

MT5 新版本把对冲账户纳进来了:同一个品种上能同时挂着 buy 和 sell,也能多品种并行持仓。引擎层面不用重写整套逻辑,关键只动 RebuildPositions 一处——它负责在每笔新交易后重排持仓列表。 原版只用 PositionSelect(symbol) 按品种挑头寸,这在净额结算账户够用,但对冲账户里同品种有多个独立头寸,按品种选会漏。新版先读 ACCOUNT_MARGIN_MODE,若不是 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 走老路;若是,则改用在循环里取 PositionGetTicket(i) 再 PositionSelectByTicket(ticket) 按订单编号精确定位。 CPosition 类在对冲与净额两种账户下是同一套,选中后都用 PositionGetInteger / PositionGetDouble / PositionGetString 读属性,所以上层策略无感。CStrategy 靠上下文判断:对冲账户上同方向多头寸就并行管,每个当成独立 CPosition;净额账户就只管那一个。 下面这段就是新旧两版 RebuildPositions 的对照,黄色高亮处是分叉改动:老版无账户类型判断、按 symbol 选;新版加了 mode 判断,对冲分支改按 ticket 选。复制进 EA 框架时,注意把 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 这行保留,否则老账户会误进对冲逻辑。 外汇与贵金属保证金交易杠杆高,对冲虽能锁敞口但占用保证金翻倍,实盘前务必在策略测试器用真实点差验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 重组持仓头寸列表
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::RebuildPositions(class="type">void)
{
  ActivePositions.Clear();
  for(class="type">int i = class="num">0; i < PositionsTotal(); i++)
  {
    class="type">class="kw">string symbol = PositionGetSymbol(i);
    PositionSelect(symbol);
    CPosition* pos = new CPosition();
    ActivePositions.Add(pos);
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 重组持仓头寸列表
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::RebuildPositions(class="type">void)
  {
  ActivePositions.Clear();
  ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
  if(mode!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
    {
      for(class="type">int i=class="num">0; i<PositionsTotal(); i++)
        {
         class="type">class="kw">string symbol=PositionGetSymbol(i);
         PositionSelect(symbol);
         CPosition *pos=new CPosition();
         ActivePositions.Add(pos);
        }
    }
  else
    {
      for(class="type">int i=class="num">0; i<PositionsTotal(); i++)
        {
         class="type">class="kw">ulong ticket=PositionGetTicket(i);
         PositionSelectByTicket(ticket);
         CPosition *pos=new CPosition();
         ActivePositions.Add(pos);
        }
    }
  }

把持仓抽象成类才好管止损平仓

把单子封装成 CPosition 类之后,EA 里对持仓的操作就不用满屏写 PositionSelect + 判断返回值,直接调成员函数即可。下面几个方法覆盖了最常见动作:取/设止损止盈、市价平仓、读手数和利润。 读取止损价的逻辑很直白:若持仓不在激活状态直接返 0.0,否则用 PositionGetDouble(POSITION_SL) 拿当前 SL。设置 SL 时则调用 m_trade.PositionModify,把新 SL 与既有 TP 一并传入,保证只改一边不误伤另一边。 平仓方法 CloseAtMarket 值得留意:它先读 ACCOUNT_MARGIN_MODE,只有当账户不是零售对冲模式时才用 PositionClose(m_symbol) 按品种平,否则用 PositionClose(m_id) 按持仓 ticket 平。netting 账户下用 symbol 平仓可能把反方向也清掉,这个分支判断能避开这类坑。外汇和贵金属保证金波动大,平仓路径写错可能意外全平,实盘前务必在策略测试器用模拟账户跑一遍。 把这些方法直接拷进你的 Include 目录,挂上 CTrade 实例就能用。下个版本可以加一个按 ATR 倍数重设 SL 的封装,让小布盯盘时自动替你挪止损。

MQL5 / C++
class="macro">#class="kw">property copyright "Copyright class="num">2016, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include "Logs.mqh"
class="macro">#include <Trade\Trade.mqh>
class="macro">#include "Trailing.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 典型策略的持仓头寸类
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPosition : class="kw">public CObject
  {
...
  };
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前头寸的止损价格
class=class="str">"cmt">//| 如果止损未设置,返回0.class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CPosition::StopLossValue(class="type">void)
{
   if(!IsActive())
      class="kw">return class="num">0.0;
   class="kw">return PositionGetDouble(POSITION_SL);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置止损价格
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPosition::StopLossValue(class="type">class="kw">double sl)
{
   if(!IsActive())
      class="kw">return class="kw">false;
   class="kw">return m_trade.PositionModify(m_id, sl, TakeProfitValue());
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前头寸的止赢价格
class=class="str">"cmt">//| 如果止赢未设置,返回0.class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CPosition::TakeProfitValue(class="type">void)
{
   if(!IsActive())
      class="kw">return class="num">0.0;
   class="kw">return PositionGetDouble(POSITION_TP);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置止赢价格
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPosition::TakeProfitValue(class="type">class="kw">double tp)
  {
   if(!IsActive())
      class="kw">return class="kw">false;
   class="kw">return m_trade.PositionModify(m_id, StopLossValue(), tp);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按当前市价平仓并设置一个平仓备注
class=class="str">"cmt">//| &class="macro">#x27;comment&class="macro">#x27;                         
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPosition::CloseAtMarket(class="type">class="kw">string comment="")
  {
   if(!IsActive())
      class="kw">return class="kw">false;
   ENUM_ACCOUNT_MARGIN_MODE mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      class="kw">return m_trade.PositionClose(m_symbol);
   class="kw">return m_trade.PositionClose(m_id);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前头寸大小               
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CPosition::Volume(class="type">void)
  {
   if(!IsActive())
      class="kw">return class="num">0.0;
   class="kw">return PositionGetDouble(POSITION_VOLUME);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回以存款货币计算的当前持仓头寸的获利
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

先用标准库把价格接口和挂单接口接好,再把持仓抽象成类统一管理止损和平仓,避免后面逻辑乱套。
Digits是价格小数位数,计算挂单偏移必须用NormalizeDouble按Digits归一化,否则会出现最小变动单位之外的废单。
可以,把代码贴给小布,它能标出对冲与净仓账户的分叉逻辑并提示常见的持仓类抽象漏洞。
在遍历持仓后按账户类型判断,对冲户逐个持仓处理,净仓户按符号汇总,分叉点放在持仓类遍历完之后最稳。
在类里只暴露修改止损、市价平仓、选平最旧/最新几个方法,主循环只调接口,别在外面直接拼交易指令。