MQL5 向导: 根据计算价位下单, 止损和止盈。标准库扩展·进阶篇
📘

MQL5 向导: 根据计算价位下单, 止损和止盈。标准库扩展·进阶篇

第 2/3 篇

「信号模块的订单参数接口与初始化」

在 MT5 的 EA 信号类里,想让多空开仓、止损、止盈和过期时间都由外部模块动态灌入,必须先声明一组虚函数。Direction() 负责汇总各信号模块的加权预测值,Prices() 则根据价格模块刷新挂单参数,两者返回 double 与 void,是后续逻辑的总入口。 OpenLongParams / OpenShortParams 用引用形参(double &price, double &sl, double &tp, datetime &expiration)把计算结果回传调用方;CheckUpdateOrderLong / CheckUpdateOrderShort 额外接收 COrderInfo* 指针,方便对已挂单做动态改单判断。 类内还暴露了十来个 getter:getOpenLong() 回传 m_order_open_long,getStopShort() 回传 m_order_stop_short,getExpLong() 回传 m_order_expiration_long,getWeight() 回传 m_weight,全部是单行 return 成员变量的轻量实现。 构造函数 CExpertSignalAdvanced() 把所有价格相关成员初始化为 EMPTY_VALUE,过期时间置 0,并把 m_price_module 设为 -1,意味着在 CalcPriceModuleIndex() 调用前,价格模块索引处于无效态。 [CODE] 里能看到,CalcPriceModuleIndex() 用 m_filters.Total() 给 m_price_module 赋值,这步必须在添加完过滤器后执行,否则 Prices() 会读到一个 -1 的索引。开仓检查函数 CheckOpenLong 的声明只写了左括号,说明具体逻辑在原文后续展开,当前节只需确认接口契约。

MQL5 / C++
class="kw">virtual class="type">class="kw">double      Direction(class="type">void);        class=class="str">"cmt">//calculating weighted average forecast based on the data received from signal modules
class="kw">virtual class="type">class="kw">double      Prices(class="type">void);        class=class="str">"cmt">//updating of parameters of the orders being placed according to the data received from price modules
class="kw">virtual class="type">bool        OpenLongParams(class="type">class="kw">double &price,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &expiration);
class="kw">virtual class="type">bool        OpenShortParams(class="type">class="kw">double &price,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &expiration);
class="kw">virtual class="type">bool        CheckUpdateOrderLong(COrderInfo *order_ptr,class="type">class="kw">double &open,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &ex);
class="kw">virtual class="type">bool        CheckUpdateOrderShort(COrderInfo *order_ptr,class="type">class="kw">double &open,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &ex);
class="type">class="kw">double              getOpenLong()        { class="kw">return m_order_open_long;     }
class="type">class="kw">double              getOpenShort()       { class="kw">return m_order_open_short;    }
class="type">class="kw">double              getStopLong()        { class="kw">return m_order_stop_long;     }
class="type">class="kw">double              getStopShort()       { class="kw">return m_order_stop_short;    }
class="type">class="kw">double              getTakeLong()        { class="kw">return m_order_take_long;     }
class="type">class="kw">double              getTakeShort()       { class="kw">return m_order_take_short;    }
class="type">class="kw">datetime            getExpLong()         { class="kw">return m_order_expiration_long;  }
class="type">class="kw">datetime            getExpShort()        { class="kw">return m_order_expiration_short; }
class="type">class="kw">double              getWeight()          { class="kw">return m_weight;              }
};
CExpertSignalAdvanced::CExpertSignalAdvanced()
  {
  m_order_open_long=EMPTY_VALUE;
  m_order_stop_long=EMPTY_VALUE;
  m_order_take_long=EMPTY_VALUE;
  m_order_expiration_long=class="num">0;
  m_order_open_short=EMPTY_VALUE;
  m_order_stop_short=EMPTY_VALUE;
  m_order_take_short=EMPTY_VALUE;
  m_order_expiration_short=class="num">0;
  m_price_module=-class="num">1;
  }
class="kw">virtual class="type">void       CalcPriceModuleIndex() {m_price_module=m_filters.Total();}
class="type">bool CExpertSignalAdvanced::CheckOpenLong(class="type">class="kw">double &price,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &expiration)
  {

多信号过滤器的方向合成逻辑

在 MT5 自定义信号类里,CheckOpenLong 先判断 m_price_module 是否为负。若未挂任何价格模块,直接回退到基类 CExpertSignal::CheckOpenLong,这一步能保证旧策略不崩。 Direction 方法才是多模块合成的核心:先用 m_weight*(LongCondition()-ShortCondition()) 算出基础方向值,再遍历前 m_price_module 个过滤器。每个过滤器用位掩码 mask=((long)1)<<i 对应忽略位 m_ignore 与反转位 m_invert,命中忽略就跳过,命中反转就把该过滤器方向值相减而非相加。 合成后若 number 不为 0,result/=number 做加权平均——这意味着挂了 3 个过滤器且都生效时,单模块极端值会被稀释到 1/3 权重,信号更平滑但也可能钝化。外汇与贵金属杠杆高,这种平滑在震荡市可能连续不触发,实盘前务必用策略测试器跑不同 m_threshold_open 观察触发频率。 Prices 方法负责清场:先把多空八项挂单参数全置为 EMPTY_VALUE 或 0,再从 m_price_module 开始循环价格模块。某模块 Prices() 返回 EMPTY_VALUE 就跳过,否则取它的 weight 参与后续开仓价计算,权重为 0 的模块实际不贡献报价。

MQL5 / C++
  class=class="str">"cmt">//--- if price modules were not found, call the method of the basic class CExpertSignal
   if(m_price_module<class="num">0)
      class="kw">return(CExpertSignal::CheckOpenLong(price,sl,tp,expiration));
   class="type">bool   result   =class="kw">false;
   class="type">class="kw">double direction=Direction();
  class=class="str">"cmt">//--- prohibitive signal
   if(direction==EMPTY_VALUE)
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- check for exceeding the threshold
   if(direction>=m_threshold_open)
     {
       Prices();
       result=OpenLongParams(price,sl,tp,expiration);class=class="str">"cmt">//there&class="macro">#x27;s a signal if m_order_open_long!=EMPTY_VALUE
     }
  class=class="str">"cmt">//--- class="kw">return the result
   class="kw">return(result);
   }
 class="type">class="kw">double CExpertSignalAdvanced::Direction(class="type">void)
   {
    class="type">long   mask;
    class="type">class="kw">double direction;
    class="type">class="kw">double result=m_weight*(LongCondition()-ShortCondition());
    class="type">int    number=(result==class="num">0.0)? class="num">0 : class="num">1;       class=class="str">"cmt">// number of queried modules
  class=class="str">"cmt">//--- loop by filters
    for(class="type">int i=class="num">0;i<m_price_module;i++)
     {
       class=class="str">"cmt">//--- mask for bitmaps(variables, containing flags)
       mask=((class="type">long)class="num">1)<<i;
       class=class="str">"cmt">//--- checking for a flag of ignoring a filter signal
       if((m_ignore&mask)!=class="num">0)
         class="kw">continue;
       CExpertSignal *filter=m_filters.At(i);
       class=class="str">"cmt">//--- checking for a pointer
       if(filter==NULL)
         class="kw">continue;
       direction=filter.Direction();
       class=class="str">"cmt">//--- prohibitive signal
       if(direction==EMPTY_VALUE)
         class="kw">return(EMPTY_VALUE);
       if((m_invert&mask)!=class="num">0)
         result-=direction;
       else
         result+=direction;
       number++;
     }
  class=class="str">"cmt">//--- averaging the sum of weighted forecasts
    if(number!=class="num">0)
       result/=number;
  class=class="str">"cmt">//--- class="kw">return the result
    class="kw">return(result);
   }
 class="type">class="kw">double CExpertSignalAdvanced::Prices(class="type">void)
   {
    m_order_open_long=EMPTY_VALUE;
    m_order_stop_long=EMPTY_VALUE;
    m_order_take_long=EMPTY_VALUE;
    m_order_expiration_long=class="num">0;
    m_order_open_short=EMPTY_VALUE;
    m_order_stop_short=EMPTY_VALUE;
    m_order_take_short=EMPTY_VALUE;
    m_order_expiration_short=class="num">0;
    class="type">int    total=m_filters.Total();
    class="type">class="kw">double last_weight_long=class="num">0;
    class="type">class="kw">double last_weight_short=class="num">0;
  class=class="str">"cmt">//--- cycle for price modules
    for(class="type">int i=m_price_module;i<total;i++)
     {  
       CExpertSignalAdvanced *prm=m_filters.At(i);
       if(prm==NULL)
         class="kw">continue;
class=class="str">"cmt">//--- ignore the current module if it has returned EMPTY_VALUE
       if(prm.Prices()==EMPTY_VALUE)class="kw">continue;
       class="type">class="kw">double weight=prm.getWeight();

◍ 权重优先的挂单参数刷新逻辑

这段信号类代码的核心,是在多模块投票里只采纳权重更高的挂单参数,并跳过权重为 0 的空信号。 if(weight==0.0) continue; 直接剔除无贡献模块;随后用 weight>last_weight_long 且到期时间大于 TimeCurrent() 的条件,保证只有「更重且未过期」的多头参数能覆盖旧值。空头分支对称处理。 OpenLongParams 负责把已缓存的开仓价、止损、止盈、过期时间写回引用参数,仅在 m_order_open_long 不等于 EMPTY_VALUE 时返回 true,否则返回 false 表示无有效挂单。 CheckUpdateOrderLong 先调 Prices() 刷新报价,再用 MathAbs 比对已有订单与缓存参数的差值是否超过 1 个 point;任一价格或过期时间不一致就重取参数,否则返回 false 不做修改。实盘里把 point 容差调到 2~3 倍,能减少 MT5 上因微小跳动触发的无谓改单。

MQL5 / C++
if(weight==class="num">0.0) class="kw">continue;
class=class="str">"cmt">//--- select non-empty values from modules with the greatest weight
if(weight>last_weight_long && prm.getExpLong()>TimeCurrent())
  if(prm.OpenLongParams(m_order_open_long,m_order_stop_long,m_order_take_long,m_order_expiration_long))
    last_weight_long=weight;
if(weight>last_weight_short && prm.getExpShort()>TimeCurrent())
  if(prm.OpenShortParams(m_order_open_short,m_order_stop_short,m_order_take_short,m_order_expiration_short))
    last_weight_short=weight;
 }
 class="kw">return(class="num">0);
}
class="type">bool CExpertSignalAdvanced::OpenLongParams(class="type">class="kw">double &price,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &expiration)
 {
 if(m_order_open_long!=EMPTY_VALUE)
   {
   price=m_order_open_long;
   sl=m_order_stop_long;
   tp=m_order_take_long;
   expiration=m_order_expiration_long;
   class="kw">return(true);
   }
 class="kw">return(class="kw">false);
 }
class="type">bool CExpertSignalAdvanced::CheckUpdateOrderLong(COrderInfo *order_ptr,class="type">class="kw">double &open,class="type">class="kw">double &sl,class="type">class="kw">double &tp,class="type">class="kw">datetime &ex)
 {
 Prices();  class=class="str">"cmt">//update prices
class=class="str">"cmt">//--- check for changes
 class="type">class="kw">double point=m_symbol.Point();
 if(   MathAbs(order_ptr.PriceOpen() - m_order_open_long)>point
    || MathAbs(order_ptr.StopLoss()  - m_order_stop_long)>point
    || MathAbs(order_ptr.TakeProfit()- m_order_take_long)>point
    || order_ptr.TimeExpiration()!=m_order_expiration_long)
    class="kw">return(OpenLongParams(open,sl,tp,ex));
class=class="str">"cmt">//--- update is not required  
 class="kw">return (class="kw">false);
 }

「EA 里只动挂单的进阶类」

CExpertAdvanced 继承自标准 CExpert,但构造和析构都是空实现,真正动刀的地方只有三个虚函数:两个检查拖曳挂单的方法和一个改单函数。它的设计意图很窄——不重新算信号,只在主信号根据最新价格给出指令后,去修改已经放置的订单。 CheckTrailingOrderLong() 把基类同名方法重写了一遍,内部先把 m_signal 转成 CExpertSignalAdvanced 指针,再调用信号的 CheckUpdateOrderLong(),把当前挂单指针和四个输出参数(price/sl/tp/ex)传进去。若信号返回 true,立刻调 UpdateOrder() 并透传结果;否则直接返回 false,不做任何操作。CheckTrailingOrderShort() 逻辑完全对称,只是走卖单分支。 UpdateOrder() 是实际落单的地方:先取挂单 ticket,若传入 price 等于 EMPTY_VALUE 就删单,否则用 OrderModify 按新价、止损、止盈和有效期去改。注意这里用的是 m_trade 的封装方法,不是直接调 OrderSend,回测里改挂单的滑点表现会和市价单走不同通道。 下面这段是类声明与两个核心方法的原文,逐行看能省掉翻标准库头文件的时间:

MQL5 / C++
class CExpertAdvanced : class="kw">public CExpert
  {
class="kw">protected:
   class="kw">virtual class="type">bool        CheckTrailingOrderLong();
   class="kw">virtual class="type">bool        CheckTrailingOrderShort();
   class="kw">virtual class="type">bool        UpdateOrder(class="type">class="kw">double price,class="type">class="kw">double sl,class="type">class="kw">double tp,class="type">class="kw">datetime ex);
class="kw">public:
                     CExpertAdvanced();
                    ~CExpertAdvanced();
  };
class="type">bool CExpertAdvanced::CheckTrailingOrderLong(class="type">void)
  {
  CExpertSignalAdvanced *signal_ptr=m_signal;
class=class="str">"cmt">//--- check for the opportunity to modify the order to buy
   class="type">class="kw">double price,sl,tp;
   class="type">class="kw">datetime ex;
   if(signal_ptr.CheckUpdateOrderLong(GetPointer(m_order),price,sl,tp,ex))
      class="kw">return(UpdateOrder(price,sl,tp,ex));
class=class="str">"cmt">//--- class="kw">return with no actions taken
   class="kw">return(class="kw">false);
  }
class="type">bool CExpertAdvanced::UpdateOrder(class="type">class="kw">double price,class="type">class="kw">double sl,class="type">class="kw">double tp,class="type">class="kw">datetime ex)
  {
   class="type">class="kw">ulong   ticket=m_order.Ticket();
   if(price==EMPTY_VALUE)
      class="kw">return(m_trade.OrderDelete(ticket));
class=class="str">"cmt">//--- modify the order, class="kw">return the result
   class="kw">return(m_trade.OrderModify(ticket,price,sl,tp,m_order.TypeTime(),ex));
  }

价格模块只重写 Prices() 这一个钩子

基础 EA 框架已经能按计算好的价位挂单和设止损,类本身也具备处理价位的能力,缺的只是把模块生成的具体级别写进去。价格模块的开发思路和交易信号模块几乎一致,唯一区别在于要覆盖的是 Prices() 而不是 LongCondition()、ShortCondition() 或 Direction()。 Prices() 在模块里专门负责价格更新,信号模块里那几个方法管的是方向判断,两者职责边界要分清楚,否则容易把价位刷新逻辑误写进条件判断里。 官方示例里附带了若干价格模块代码可直接参考,读者若对信号模块开发流程不熟,建议先回看「六步创建交易机器人」与「基于定制指标生成信号」两篇再动手。外汇与贵金属品种波动剧烈,模块生成的价位仅作概率参考,实盘前务必在 MT5 策略测试器跑多周期验证。

◍ 用增量之字折线切出价格模块的入场与止损

增量之字折线指标会按设定的最近峰值数量画出若干级别,当价格穿越这些级别时,趋势倾向反转。价格模块的任务就是从指标缓冲区里抓出入场级别,再贴着最近的局部极值放止损,用止损乘以系数推止盈。 描述符里前 5 个参数必不可少:应用价格(0 收价 / 1 高低价)、反转模式(0 点值 / 1 百分比)、反转点值、反转百分比、峰值数量。后面 setTpRatio 默认 1.6 是止盈对止损的倍数,setExpBars 默认 10 是订单过期柱数,这几个直接决定仓位的风险跨度。 Prices() 是模块核心。它从第 3、第 4 缓存区分别取买、卖开仓价,买和卖条件互斥;先查买单,有价就调 getBuySL() 找局部最小作止损,再算止盈与过期时间,卖单对称处理。若返回 EMPTY_VALUE,主信号会直接忽略这批参数。 getBuySL() / getSellSL() 只认相应缓存区里最后一个非零值当作最近局部极值。你在 MT5 里挂这个模块时,把 setLevels 从 2 改成 3 或 4,能明显看到级别变密、触发更频繁,但假突破概率也可能上升。外汇与贵金属杠杆高,参数乱调容易连续止损。 构造器先给参数填省缺值,等 EA 输入参数进来再覆盖;ValidationSettings() 验参失败会在日志报错并返回 false,InitDeltaZZ() 才真正把定制指标塞进集合。三者跑通后模块才就绪。

MQL5 / C++
class=class="str">"cmt">// wizard description start
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Description of the class                                          |
class=class="str">"cmt">//| Title=DeltaZZ Price Module                                       |
class=class="str">"cmt">//| Type=SignalAdvanced                                              |
class=class="str">"cmt">//| Name=DeltaZZ Price Module                                        |
class=class="str">"cmt">//| ShortName=DeltaZZ_PM                                             |
class=class="str">"cmt">//| Class=CPriceDeltaZZ                                              |
class=class="str">"cmt">//| Page=not used                                                    |
class=class="str">"cmt">//| Parameter=setAppPrice,class="type">int,class="num">1, Applied price: class="num">0 - Close, class="num">1 - H/L    |
class=class="str">"cmt">//| Parameter=setRevMode,class="type">int,class="num">0, Reversal mode: class="num">0 - Pips, class="num">1 - Percent |
class=class="str">"cmt">//| Parameter=setPips,class="type">int,class="num">300,Reverse in pips                        |
class=class="str">"cmt">//| Parameter=setPercent,class="type">class="kw">double,class="num">0.5,Reverse in percent               |
class=class="str">"cmt">//| Parameter=setLevels,class="type">int,class="num">2,Peaks number                           |
class=class="str">"cmt">//| Parameter=setTpRatio,class="type">class="kw">double,class="num">1.6,TP:SL ratio                      |
class=class="str">"cmt">//| Parameter=setExpBars,class="type">int,class="num">10,Expiration after bars number         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// wizard description end
class CPriceDeltaZZ : class="kw">public CExpertSignalAdvanced
  {
class="kw">protected:
   CiCustom            m_deltazz;          class=class="str">"cmt">//object of the DeltaZZ indicator
   class=class="str">"cmt">//--- module settings
   class="type">int                 m_app_price;
   class="type">int                 m_rev_mode;
   class="type">int                 m_pips;
   class="type">class="kw">double              m_percent;

常见问题

用进阶类单独接管挂单刷新逻辑,在订单遍历时只筛选 pending 类型,改完即返回,不触发市价单重算。
按权重优先规则叠加各信号多空值,净权重为正开多、为负开空,临界附近可设阈值忽略弱信号。
可以。小布能读取你的策略配置,标出权重冲突或挂单刷新遗漏的环节,并给出修正建议。
只重写 Prices() 这一个钩子,用增量之字折线切出入场与止损位,其余周期计算保持原样。
信号模块订单参数接口接收的是计算好的具体价位,初始化时把点位换算成绝对价再传入即可。