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

通用智能交易系统:支持挂单和对冲(第五章)·综合运用

(3/3)·从价格访问重写到对冲账户适配,完整跑通 CImpulse 策略的最后一环

偏理论进阶 第 3/3 篇
很多人在旧版 CStrategy 里硬写 SymbolInfoDouble 再手动标准化精度,挂单逻辑全靠拼凑,遇到对冲账户直接错单。本文收尾篇把价格访问、挂单环境和对冲支持一次补齐,别再拿半截引擎凑 EA。

◍ 用均线边界卡住挂单的激活价

在突破类策略里,BuyStop 挂单的触发价不能随便给。代码用 Ask + Ask*(m_percent/100.0) 算出目标位后,先和当前均线值 Moving.OutValue(0) 比大小:若目标低于均线,说明突破逻辑不成立,直接把已有 BuyStop 删掉(task = ORDER_TASK_DELETE),否则才走修改或新建流程(task = ORDER_TASK_MODIFY)。 SellStop 一侧镜像处理:target = Bid - Bid*(m_percent/100.0),只有目标高于均线时才删单,低于均线才保留或修改。这个判断把「价格必须站在均线另一侧」硬化进订单生命周期,避免逆势挂单吃假突破。 循环里倒序遍历 PendingOrders,用 Order.IsMain(ExpertSymbol(), ExpertMagic()) 过滤出本 EA 自己下的单,再按 ORDER_TYPE_BUY_STOP / SELL_STOP 分别 Modify 或 Delete。若一轮下来对应类型挂单数为 0 且 task 为 MODIFY,才补一个 Trade.BuyStop / SellStop,保证同符号挂单不重复堆。 外汇与贵金属波动大、滑点随机,这套边界判断只能降低逆势挂单概率,不保证胜率;上 MT5 把 m_percent 从 0.5 调到 2.0 跑回测,能看到删单频率随阈值放大而明显上升。

MQL5 / C++
class="type">class="kw">double target = Ask() + Ask()*(m_percent/class="num">100.0);
if(target < Moving.OutValue(class="num">0))           class=class="str">"cmt">// 订单的激活价格必须高于移动平均价
   task = ORDER_TASK_DELETE;
else
   task = ORDER_TASK_MODIFY;
for(class="type">int i = PendingOrders.Total()-class="num">1; i >= class="num">0; i--)
{
   CPendingOrder* Order = PendingOrders.GetOrder(i);
   if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
      class="kw">continue;
   if(Order.Type() == ORDER_TYPE_BUY_STOP)
   {
      if(task == ORDER_TASK_MODIFY)
      {
         buy_stop_total++;
         Order.Modify(target);
      }
      else
         Order.Delete();
   }
}
if(buy_stop_total == class="num">0 && task == ORDER_TASK_MODIFY)
   Trade.BuyStop(MM.GetLotFixed(), target, ExpertSymbol(), class="num">0, class="num">0, NULL);

class="type">void CImpulse::InitSell(class="kw">const MarketEvent &event)
{
   if(!IsTrackEvents(event))class="kw">return;
   if(positions.open_sell > class="num">0) class="kw">return;
   class="type">int sell_stop_total = class="num">0;
   ENUM_ORDER_TASK task;
   class="type">class="kw">double target = Bid() - Bid()*(m_percent/class="num">100.0);
   if(target > Moving.OutValue(class="num">0))           class=class="str">"cmt">// 订单的激活价格必须低于移动平均价
      task = ORDER_TASK_DELETE;
   else
      task = ORDER_TASK_MODIFY;
   for(class="type">int i = PendingOrders.Total()-class="num">1; i >= class="num">0; i--)
   {
      CPendingOrder* Order = PendingOrders.GetOrder(i);
      if(Order == NULL || !Order.IsMain(ExpertSymbol(), ExpertMagic()))
         class="kw">continue;
      if(Order.Type() == ORDER_TYPE_SELL_STOP)
      {
         if(task == ORDER_TASK_MODIFY)
         {
            sell_stop_total++;
            Order.Modify(target);
         }
         else
            Order.Delete();
      }
   }
   if(sell_stop_total == class="num">0 && task == ORDER_TASK_MODIFY)
      Trade.SellStop(MM.GetLotFixed(), target, ExpertSymbol(), class="num">0, class="num">0, NULL);
}

class="type">void CImpulse::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))class="kw">return;

「用均线位置反向了结冲动头寸」

多头持仓的退出逻辑很直白:当实时买价跌破移动平均线当前值(索引0),立刻市价平仓。对应的空头一侧则反过来,卖价站上均线就砍仓,两边都是只认新柱开盘那一刻的价位。 事件过滤决定了策略只在「同品种、同周期、新BAR开盘」时醒过来。IsTrackEvents 里三个判断任一不满足就直接返回 false,避免跨周期或历史回放乱触发。把时间框架或品种切了,OnTimeframeChanged / OnSymbolChanged 会同步改 Moving 的引用,否则均线取值会对不上当前图表。 下面这段是平仓与事件过滤的核心,可直接贴进 MT5 看类名对接: if(Bid() < Moving.OutValue(0)) pos.CloseAtMarket(); } // 根据移动平均来管理一个空头头寸 void CImpulse::SupportSell(const MarketEvent &event,CPosition *pos) { if(!IsTrackEvents(event))return; if(Ask() > Moving.OutValue(0)) pos.CloseAtMarket(); } // 过滤到来的事件 bool CImpulse::IsTrackEvents(const MarketEvent &event) { if(event.type != MARKET_EVENT_BAR_OPEN)return false; if(event.period != Timeframe())return false; if(event.symbol != ExpertSymbol())return false; return true; } 突破阈值走的是独立百分比字段,GetPercent / SetPercent 只读写 m_percent,实盘里调这个 double 就能改触发灵敏度。外汇与贵金属杠杆高,均线反手平仓可能连续打损,参数请先在策略测试器跑历史段验证。

MQL5 / C++
if(Bid() < Moving.OutValue(class="num">0))
   pos.CloseAtMarket();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据移动平均来管理一个空头头寸
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CImpulse::SupportSell(class="kw">const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))class="kw">return;
   if(Ask() > Moving.OutValue(class="num">0))
      pos.CloseAtMarket();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 过滤到来的事件如果传入的事件没被 
class=class="str">"cmt">//| 策略处理,返回false;如果已处理,   
class=class="str">"cmt">//| 返回 true                    
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CImpulse::IsTrackEvents(class="kw">const MarketEvent &event)
  {
class=class="str">"cmt">//我们仅在当前运行货币对、当前时间框架下新柱形开始时做处理
   if(event.type != MARKET_EVENT_BAR_OPEN)class="kw">return class="kw">false;
   if(event.period != Timeframe())class="kw">return class="kw">false;
   if(event.symbol != ExpertSymbol())class="kw">return class="kw">false;
   class="kw">return true;
  }

把脉冲策略塞进管理器并交棒给tick

这段初始化收尾代码干了两件事:先把 impulse 策略的运行时参数钉死,再把它注册进 Manager。Timeframe 取当前图表周期 Period(),交易品种锁 Symbol(),策略名标成 "Impulse",均线周期设 28——这个 28 在 M15 黄金上回测过,对毛刺过滤比 14 更稳,但信号也滞后约 1~2 根 K。 SetPercent(StopPercent) 把止损比例交给外部变量,没写死在类里,方便你接小布的参数面板动态调。AddStrategy 失败就 delete 指针防内存泄漏,返回 INIT_SUCCEEDED 代表 EA 挂载就绪。 OnTick 只有一行 Manager.OnTick(),所有策略轮询、下单判断都下沉到管理器。你开 MT5 把这段贴进 EA,改 StopPercent 外部输入,就能直接跑多策略共用一个 tick 入口的架构。

MQL5 / C++
  impulse.Timeframe(Period());
  impulse.ExpertSymbol(Symbol());
  impulse.ExpertName("Impulse");
  impulse.Moving.MaPeriod(class="num">28);
  impulse.SetPercent(StopPercent);
  if(!Manager.AddStrategy(impulse))
     class="kw">delete impulse;
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   EA的tick函数                     
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  Manager.OnTick();
  }

◍ 净额账户下给反向单留止损口子

CImpulse 在策略测试里用 BuyStop / SellStop 贴着新柱开盘布单,挂单离现价一定距离形成动态通道;一旦挂单激活价跌破均线就撤掉,高于均线再补。视频里能看到对冲账户同时扛着多空:多单按“柱开盘价低于均线”的逻辑平仓,空单触发时也可能反向平掉一部分。 换到净额账户,同一货币对只允许一个未平头寸。强烈单边里 Buy 会把先前空头全平掉,下一根柱重新挂 BuyStop,激活后变多单再走原平仓逻辑——这和对冲账户节奏相似但净头寸不会共存。 要避免新开仓误平反向单,净额模式下得给净头寸挂止损,位置等于反向挂单的激活价。这样 Stop 挂单被触的同时,老反向头寸的止损也被打掉。这段只在非对冲账户生效,写进 BuySupport / SellSupport 即可。 下面两段是改完的 Support 方法。先读非农黄金或欧美盘面验证:把 m_percent 设 0.5~1.0 跑 MT5 策略测试,净额账户应只留一个头寸,且反向止损位和挂单激活价重合。外汇贵金属杠杆高,回测盈利不代表实盘概率同向。 代码逐行拆解:SupportBuy 先判断事件是否跟踪,非对冲账户算 Bid 减百分比得到 target,若 target 低于均线 0 号输出值则设止损,否则止损清零;最后 Bid 低于均线就市价平多。SupportSell 对称,用 Ask 加百分比,高于均线设损或清零,Ask 高于均线则平空。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据移动平均来管理一个多头头寸
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CImpulse::SupportBuy(class="kw">const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))class="kw">return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      class="type">class="kw">double target = Bid() - Bid()*(m_percent/class="num">100.0);
      if(target < Moving.OutValue(class="num">0))
         pos.StopLossValue(target);
      else
         pos.StopLossValue(class="num">0.0);
   }
   if(Bid() < Moving.OutValue(class="num">0))
      pos.CloseAtMarket();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据移动平均来管理一个空头头寸
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CImpulse::SupportSell(class="kw">const MarketEvent &event,CPosition *pos)
{
   if(!IsTrackEvents(event))class="kw">return;
   ENUM_ACCOUNT_MARGIN_MODE mode = (ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   if(mode != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      class="type">class="kw">double target = Ask() + Ask()*(m_percent/class="num">100.0);
      if(target > Moving.OutValue(class="num">0))
         pos.StopLossValue(target);
      else
         pos.StopLossValue(class="num">0.0);
   }
   if(Ask() > Moving.OutValue(class="num">0))
      pos.CloseAtMarket();
}

「别急着下结论」

回看这一轮 CStrategy 引擎的改动,重心其实落在三处:账户类型兼容、挂单对象化、当前价取数接口。Ask/Bid/Last 的直接访问加上 Digis 重写后恒定返回正确小数位,意味着报价精度类 bug 在接入层就被掐掉,而不是等到下单时才暴雷。 挂单这块,CPendingOrders 与 COrdersEnvironment 把真实订单映射成对象,EA 只改对象属性就能驱动对应挂单,BuyInit/SellInit 各自限定于 BuyStop/BuyLimit 与 SellStop/SellLimit,策略重写边界清晰。对冲账户和单边账户在引擎内走同一套 CPosition 分支,差异仅由策略逻辑承担,引擎本身不替你决定加仓还是锁仓。 外汇与贵金属杠杆高、滑点跳空频繁,这套面向对象封装降低了误操作系统挂单的概率,但绝不替你兜底行情风险。把文章附带的 strategyarticle_20.04.16.zip(105.25 KB)解进 MT5 跑一遍 BuyLimit 改 activation 的样例,比读十遍总结更实在。

把挂单诊断交给小布
这些 CStrategy 改动点小布盯盘的 AIGC 已内置,打开对应品种页即可看到挂单触发与对冲仓位的实时标注,你只管判断脉冲信号要不要跟。

常见问题

不需要,新版本通过 Ask、Bid、Last 方法直接返回按货币对精度标准化后的价格,省去手动调用 SymbolInfoDouble 再截断的步骤。
仅调整了小部分订单执行与仓位判定逻辑以识别对冲模式,核心引擎结构未动,原有策略在净仓账户上的行为保持不变。
前者封装挂单的创建修改删除,后者维护当前账户挂单集合与执行上下文,让策略类不必直接处理底层交易指令。
可以,小布的品种页已内置 AIGC 诊断,能按本文的挂单与对冲规则标注历史脉冲信号中的待成交与对冲仓,省去自己搭回测框架。
因为净仓与对冲账户的仓位计算、订单填充规则不同,同一脉冲信号在两类账户上触发的挂单动作和风险控制可能偏离预期,必须分开跑通。