MQL5 向导: 根据计算价位下单, 止损和止盈。标准库扩展·进阶篇
「信号模块的订单参数接口与初始化」
在 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 的声明只写了左括号,说明具体逻辑在原文后续展开,当前节只需确认接口契约。
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 的模块实际不贡献报价。
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 上因微小跳动触发的无谓改单。
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,回测里改挂单的滑点表现会和市价单走不同通道。 下面这段是类声明与两个核心方法的原文,逐行看能省掉翻标准库头文件的时间:
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() 才真正把定制指标塞进集合。三者跑通后模块才就绪。
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;