MQL5 向导: 根据计算价位下单, 止损和止盈。标准库扩展
介绍
MQL5 标准库 是开发大型需要严谨体系的项目的有用援助。MQL5 向导 以对话模式在几分钟里就可以将现成的零件组装进广泛的规划中, 这种能力不可低估。MQL5 向导自动将 EA 的各个零件收集到一起,并根据它们的句柄自动在 EA 里声明模块参数。当有大量不同模块集成, 这种自动化可以节省大量时间和常规操作。
这听起来不错但有一个明显的缺点 - 通过向导在标准类基础上创建的交易系统,其能力有限。本文研究一种通用方法,可以显著扩展所创建的 EA 功能。当此方法被实现,可与向导和标准模块保持相同的兼容性。
此方法的思路是使用 面向对象编程 的继承和多态机制,或创建类来代替生成 EA 时的标准类代码。通过这种方式,向导和标准库德所有优点都可以被利用,结果就是开发出来的 EA 具有所需的能力。如此达成目的, 代码量降低了一点, 仅有四个字符串。
本文的实际目的是增加生成的 EA 能力,在所要求的价位下单,止损和止盈,而不仅仅从当前价位到指定的距离。
类似的思路在 "MQL5 向导: 如何教导 EA 在任意价位挂单" 一文中也有论述。而建议方案的明显缺点就是“强制”改变来自从属过滤器的交易信号模块的参数。这种方法不会过渡集成大量模块一起工作,并且使用向导来进行优化是没有意义的。
在继承自标准库的类中实现任意价位下单、止损和止盈将在稍后详细研究。也就是说, 模块之间的任何冲突都是不可能的。我希望这篇文章提供的例子可以激发读者编写自己的代码改善标准框架,并且也允许用户实现开发库扩展。
1. 标准决策算法
由 MQL5 向导生成的 EA 基于 CExpert 类实例。对象 CExpertSignal 类的指针在此类中声明。此外,处于简洁的缘故,在本文中该对象将调用主要信号。主信号包含指向从属过滤器 (信号模块是 CExpertSignal 类的继承者)。
如果没有持仓和订单,则 EA 会在新的即时报价到达时参考主信号来检查开仓时机。主信号逐个轮询从属滤波器,并基于所获得的预测,计算加权平均预测 (方向)。如果它的值超过阀值 (主信号的 m_threshold_open 参数值), 订单参数和 bool 类型的条件检查结果将会传递到 EA。如果这些条件符合, 或是在市场价开仓, 或是在一定距离上挂单 (参看图例. 1)。止损只能置于固定距离。自市场价的开仓、止损和止盈距离在 EA 设置里指定,并分别保存在主要信号的 m_price_level, m_stop_level 和 m_take_level 变量中。
所以,当前两个条件都满足,则订单被放置:
- 当前品种没有持仓;
- 加权平均预测值的绝对值超过阀值, 意即趋势极强。
图例. 1. 入场的决策形态
当前图例. 1 中的决策形态明显限制了 MQL5 向导的应用领域。以固定值止损的策略在长线交易中因为多变的波动性罕有成效。系统采用挂单时,通常需要将它们放置在动态计算的价位。
2. 修改的决策算法
从价位计算和放置订单的角度来看, 这是一个死结, 由于信号模块不能生成除了预测值之外的任何东西, 且主要信号并非设计用来与价位工作。在这方面,它的建议是:
- 引入一种新型信号模块 (我们打算称它们为价格模块) 能够生成订单参数;
- 训练主要信号来处理这些参数,即,选择最佳者并将它们传递给 EA。
修改的算法 (图例. 2) 可与除了挂单之外的其它请求工作。该算法的精髓在于它将定义的趋势 (加权平均预测) 与入场点 (价位) 分离。这意味着决策时,筛选过的最优交易方向已经定义,且一组订单参数已经从选择的合适方向的价格模块中获取。如果有若干类似的集合可用, 则接受来自模块的具有最大权重的集合 (参数值 m_weight 被选择)。如果方向已经确定但当前没有入场点可用, 则 EA 处于未激活状态。
图例. 2. 修改的入场决策形态
为了放置新订单, 以下需求必须满足:
- 当前品种无持仓以及其它订单;
- 加权平均预测值的绝对值超过阀值;
- 至少发现一个订单的开盘价。
图例. 2 中的算法可以处理许多可能的入场点,通过方向筛选它们,并在 EA 中 选择最好的一个。
3. 开发修改的 EA 类和信号模块
库的扩展基于两个类: CExpertSignalAdvanced 和 CExpertAdvanced, 分别继承自 CExpertSignal 和 CExpert。
所有添加新能力的措施,瞄准在改变 EA 不同模块之间交换数据的接口。例如, 为了实现图例.2 的形态算法, 它需要组织带有价格模块 (这个类的后代) 的主要信号 (类 CExpertSignalAdvanced) 的交互。当数据改变,更新已放置的订单,意味着 EA (类 CExpertAdvanced) 与主要信号交互。
所以在此阶段,我们将实现图例. 2 中的形态,在参数变化时开仓并组织更新已放置的订单 (例如, 当更佳入场点出现时)。让我们来研究 CExpertSignalAdvanced 类。
3.1. CExpertSignalAdvanced
这个类将要替换其前辈在主要信号中的角色,并成为价格模块的基础,如同其前辈是信号模块的基础。
class CExpertSignalAdvanced : public CExpertSignal { protected: //---data members for storing parameters of the orders being placed double m_order_open_long; //opening price of the order to buy double m_order_stop_long; //Stop Loss of the order to buy double m_order_take_long; //Take Profit of the order to buy datetime m_order_expiration_long; //expiry time of the order to buy double m_order_open_short; //opening price of the order to sell double m_order_stop_short; //Stop Loss of the order to sell double m_order_take_short; //Take Profit of the order to sell datetime m_order_expiration_short; //expiry time of the order to sell //--- int m_price_module; //index of the first price module in the m_filters array public: CExpertSignalAdvanced(); ~CExpertSignalAdvanced(); virtual void CalcPriceModuleIndex() {m_price_module=m_filters.Total();} virtual bool CheckOpenLong(double &price,double &sl,double &tp,datetime &expiration); virtual bool CheckOpenShort(double &price,double &sl,double &tp,datetime &expiration); virtual double Direction(void); //calculating weighted average forecast based on the data received from signal modules virtual double Prices(void); //updating of parameters of the orders being placed according to the data received from price modules virtual bool OpenLongParams(double &price,double &sl,double &tp,datetime &expiration); virtual bool OpenShortParams(double &price,double &sl,double &tp,datetime &expiration); virtual bool CheckUpdateOrderLong(COrderInfo *order_ptr,double &open,double &sl,double &tp,datetime &ex); virtual bool CheckUpdateOrderShort(COrderInfo *order_ptr,double &open,double &sl,double &tp,datetime &ex); double getOpenLong() { return m_order_open_long; } double getOpenShort() { return m_order_open_short; } double getStopLong() { return m_order_stop_long; } double getStopShort() { return m_order_stop_short; } double getTakeLong() { return m_order_take_long; } double getTakeShort() { return m_order_take_short; } datetime getExpLong() { return m_order_expiration_long; } datetime getExpShort() { return m_order_expiration_short; } double getWeight() { return m_weight; } };
在 CExpertSignalAdvanced 类中,保存订单参数的得数据成员已经被声明。这些变量的数值在 Prices() 方法中得到更新。这些变量充当缓冲器。
而参数 m_price_module 得以声明。它在 CExpertSignal 中声明的数组 m_filters 里保存价格模块的索引。这个数组包含信号模块的指针。标准模块 (过滤器) 的指针保存在数组的开头。那么, 自 m_price_module 索引开始, 轮到价格模块。为了避免改变指标和时间序列初始化方法的必要性,决定在数组中保存所有东西。而且, 可以通过一个数组保存 64 个模块, 一般情况下这已经足够了。
此外, 帮助方法也已经在 CExpertSignalAdvanced 类中声明, 用来获取保护性数据成员的数值。它们的名字以 get 开头 (参看类声明)。
3.1.1. 构造器
类 CExpertSignalAdvanced 的构造器初始化类内部声明的变量:
CExpertSignalAdvanced::CExpertSignalAdvanced() { m_order_open_long=EMPTY_VALUE; m_order_stop_long=EMPTY_VALUE; m_order_take_long=EMPTY_VALUE; m_order_expiration_long=0; m_order_open_short=EMPTY_VALUE; m_order_stop_short=EMPTY_VALUE; m_order_take_short=EMPTY_VALUE; m_order_expiration_short=0; m_price_module=-1; }
3.1.2. CalcPriceModuleIndex()
方法 CalcPriceModuleIndex() 在 m_price_module 中分配当前数组元素的数量, 它等于随后添加的模块索引。这个方法在添加首个价格模块时被调用。函数体在类的声明里。
virtual void CalcPriceModuleIndex() {m_price_module=m_filters.Total();}
3.1.3. CheckOpenLong(...) 和 CheckOpenShort(...)
方法 CheckOpenLong(...) 从 CExpert 类的实例中调用,且按照如下描述工作:
- 检查包含的价格模块。如果没有, 则调用父类的同名方法;
- 接收来自 Direction() 方法的加权平均预测 (方向);
- 通过比较方向值与空值,以及 m_threshold_open 中的阀值,验证入场条件是否满足;
- 通过 Prices() 方法更新订单参数值, 并以 OpenLongParams(...) 函数传递它们至 EA。保存此函数的结果;
- 返回保存的结果。
bool CExpertSignalAdvanced::CheckOpenLong(double &price,double &sl,double &tp,datetime &expiration) { //--- if price modules were not found, call the method of the basic class CExpertSignal if(m_price_module<0) return(CExpertSignal::CheckOpenLong(price,sl,tp,expiration)); bool result =false; double direction=Direction(); //--- prohibitive signal if(direction==EMPTY_VALUE) return(false); //--- check for exceeding the threshold if(direction>=m_threshold_open) { Prices(); result=OpenLongParams(price,sl,tp,expiration);//there's a signal if m_order_open_long!=EMPTY_VALUE } //--- return the result return(result); }
CheckOpenShort(...) 具有相同的工作原理,并用同样的方式,这就是为什么我们不会去研究它。
3.1.4 Direction()
方法 Direction() 查询过滤器并计算加权平均预测。此方法其父类 CExpertSignal 中的同名方法十分类似,带有异常,在循环中,我们不能引用所有 m_filters 数组内的元素,而只有那些索引从 0 到小于 m_price_module的。所有东西均类似 CExpertSignal::Direction()。
double CExpertSignalAdvanced::Direction(void) { long mask; double direction; double result=m_weight*(LongCondition()-ShortCondition()); int number=(result==0.0)? 0 : 1; // number of queried modules //--- loop by filters for(int i=0;i<m_price_module;i++) { //--- mask for bitmaps (variables, containing flags) mask=((long)1)<<i; //--- checking for a flag of ignoring a filter signal if((m_ignore&mask)!=0) continue; CExpertSignal *filter=m_filters.At(i); //--- checking for a pointer if(filter==NULL) continue; direction=filter.Direction(); //--- prohibitive signal if(direction==EMPTY_VALUE) return(EMPTY_VALUE); if((m_invert&mask)!=0) result-=direction; else result+=direction; number++; } //--- averaging the sum of weighted forecasts if(number!=0) result/=number; //--- return the result return(result); }
3.1.5. Prices()
方法 Prices() 遍历 m_filters 数组的第二部分,从 m_price_module 指定的索引开始一直到结束。查询价格模块并通过函数 OpenLongParams(...) 和 OpenShortParams(...) 更新类变量的值。在循环之前,参数值被清除。
在循环中,如果当前价格模块 (m_weight) 的权重大于之前查询模块提供的数值,参数值被改写。作为结果, 或是保留空参数值 (如果未发现任何东西) 或参数为调用方法之时得到的最佳权重。
double CExpertSignalAdvanced::Prices(void) { m_order_open_long=EMPTY_VALUE; m_order_stop_long=EMPTY_VALUE; m_order_take_long=EMPTY_VALUE; m_order_expiration_long=0; m_order_open_short=EMPTY_VALUE; m_order_stop_short=EMPTY_VALUE; m_order_take_short=EMPTY_VALUE; m_order_expiration_short=0; int total=m_filters.Total(); double last_weight_long=0; double last_weight_short=0; //--- cycle for price modules for(int i=m_price_module;i<total;i++) { CExpertSignalAdvanced *prm=m_filters.At(i); if(prm==NULL) continue; //--- ignore the current module if it has returned EMPTY_VALUE if(prm.Prices()==EMPTY_VALUE)continue; double weight=prm.getWeight(); if(weight==0.0)continue; //--- 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; } return(0); }
3.1.6. OpenLongParams(...) 和 OpenShortParams(...)
在 CExpertSignalAdvanced 类之内, 方法 OpenLongParams(...) 传递订单参数值来买入,参照来自类变量的输入参数。
此方法与其父类中的略有不同。它过去基于市场价格和主要信号中的指定缩进计算所需的参数值。现在,只需要传递准备好的参数。如果开仓价正确 (不等于 EMPTY_VALUE), 则方法返回 true, 否则 false。
bool CExpertSignalAdvanced::OpenLongParams(double &price,double &sl,double &tp,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; return(true); } return(false); }
我们不打算研究 OpenShortParams(...) 因为它的操作原理相同,且它的使用也类似。
3.1.7. CheckUpdateOrderLong(...) 和 CheckUpdateOrderShort(...)
方法 CheckUpdateOrderLong(...) 和 CheckUpdateOrderShort(...) 在 CExpertAdvanced 类中调用。它们用来根据最后的价位更新已经放置的订单。
我们将更深入地检查方法 CheckUpdateOrderLong(...)。当调用方法 Prices(...) 时,第一个价位得到更新, 之后数据更新检查是为了排除可能的修改错误。最后, 方法 OpenLongParams(...) 被调用来传递更新数据并返回结果。
bool CExpertSignalAdvanced::CheckUpdateOrderLong(COrderInfo *order_ptr,double &open,double &sl,double &tp,datetime &ex) { Prices(); //update prices //--- check for changes 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) return(OpenLongParams(open,sl,tp,ex)); //--- update is not required return (false); }
CheckUpdateOrderShort(...) 不打算研究了,因为其操作和应用方法雷同。
3.2. CExpertAdvanced
在 EA 类中的改变,仅关注根据主要信号里的价格更新数据修改已经放置的订单。类 CExpertAdvanced 声明如下所示。
class CExpertAdvanced : public CExpert { protected: virtual bool CheckTrailingOrderLong(); virtual bool CheckTrailingOrderShort(); virtual bool UpdateOrder(double price,double sl,double tp,datetime ex); public: CExpertAdvanced(); ~CExpertAdvanced(); };
正如我们所看到的,方法很少,构造和析构都是空的。
3.2.1. CheckTrailingOrderLong() 和 CheckTrailingOrderShort()
方法 CheckTrailingOrderLong() 覆盖基础类中的同名方法,并调用主要信号的方法 CheckUpdateOrderLong(...) 来找出必须要修改的订单。如果需要修改, 方法 UpdateOrder(...) 被调用并返回得到的结果。
bool CExpertAdvanced::CheckTrailingOrderLong(void) { CExpertSignalAdvanced *signal_ptr=m_signal; //--- check for the opportunity to modify the order to buy double price,sl,tp; datetime ex; if(signal_ptr.CheckUpdateOrderLong(GetPointer(m_order),price,sl,tp,ex)) return(UpdateOrder(price,sl,tp,ex)); //--- return with no actions taken return(false); }
方法 CheckTrailingOrderShort() 类似,使用方式也相同。
3.2.2. UpdateOrder()
函数 UpdateOrder() 开始检查相关的价格 (非 EMPTY_VALUE)。如果为空, 订单删除, 否则订单根据接收的参数进行修改。
bool CExpertAdvanced::UpdateOrder(double price,double sl,double tp,datetime ex) { ulong ticket=m_order.Ticket(); if(price==EMPTY_VALUE) return(m_trade.OrderDelete(ticket)); //--- modify the order, return the result return(m_trade.OrderModify(ticket,price,sl,tp,m_order.TypeTime(),ex)); }
开发标准类的继承者完成。
4. 开发价格模块
我们已经有了创建 EA 的基础, 在计算的价位上放置订单及止损。准确地说,我们的类已经准备好与价位工作。现在只留下模块生成的这些级别要被写入。
开发价格模块的过程类似编写交易信号模块。它们之间仅有的不同在于它的 Prices(), 在模块内此方法负责价格更新, 需要被覆盖, 而不像信号模块里的 LongCondition(), ShortCondition() 或 Direction()。理想情况下,读者应该对信号模块开发有一个清晰的概念。文章 "在 6 步里创建您自己的交易机器人!" 和 "基于定制指标生成交易信号" 也许对此有帮助。
若干价格模块的代码作为例子提供。
4.1. 基于 "增量之字折线" 指标的价格模块
指标 增量之字折线 通过指定的最后若干数量的峰值绘制级别。如果价位与这些级别交叉, 它意味着趋势可能反转。
价格模块的目的是从指标缓冲区里取得入场级别,找到最近的局部极值放置止损,计算止损乘以在设置中指定的系数得到止盈。
图例. 3. 描绘按照增量之字折线指标操作,使用订单买入的例子。
图例 3. 所示价位由价格模块生成。在订单触发之前, 止损和止盈根据以下最小更新变化。
4.1.1. 模块描述符
// wizard description start //+------------------------------------------------------------------+ //| Description of the class | //| Title=DeltaZZ Price Module | //| Type=SignalAdvanced | //| Name=DeltaZZ Price Module | //| ShortName=DeltaZZ_PM | //| Class=CPriceDeltaZZ | //| Page=not used | //| Parameter=setAppPrice,int,1, Applied price: 0 - Close, 1 - H/L | //| Parameter=setRevMode,int,0, Reversal mode: 0 - Pips, 1 - Percent | //| Parameter=setPips,int,300,Reverse in pips | //| Parameter=setPercent,double,0.5,Reverse in percent | //| Parameter=setLevels,int,2,Peaks number | //| Parameter=setTpRatio,double,1.6,TP:SL ratio | //| Parameter=setExpBars,int,10,Expiration after bars number | //+------------------------------------------------------------------+ // wizard description end
描述符中的前五个参数是必需的,用于设置使用的指标。随后的系数用于从当前的价格模块中计算基于止损的止盈,和订单的过期柱线时间。
4.1.2. 类声明
class CPriceDeltaZZ : public CExpertSignalAdvanced { protected: CiCustom m_deltazz; //object of the DeltaZZ indicator //--- module settings int m_app_price; int m_rev_mode; int m_pips; double m_percent; int m_levels; double m_tp_ratio; //tp:sl ratio int m_exp_bars; //lifetime of the orders in bars //--- method of indicator initialization bool InitDeltaZZ(CIndicators *indicators); //--- helper methods datetime calcExpiration() { return(TimeCurrent()+m_exp_bars*PeriodSeconds(m_period)); } double getBuySL(); //function for searching latest minimum of ZZ for buy SL double getSellSL(); //function for searching latest maximum of ZZ for sell SL public: CPriceDeltaZZ(); ~CPriceDeltaZZ(); //--- methods of changing module settings void setAppPrice(int ap) { m_app_price=ap; } void setRevMode(int rm) { m_rev_mode=rm; } void setPips(int pips) { m_pips=pips; } void setPercent(double perc) { m_percent=perc; } void setLevels(int rnum) { m_levels=rnum; } void setTpRatio(double tpr) { m_tp_ratio=tpr; } void setExpBars(int bars) { m_exp_bars=bars;} //--- method of checking correctness of settings virtual bool ValidationSettings(void); //--- method of creating indicators virtual bool InitIndicators(CIndicators *indicators); //--- main method of price module updating the output data virtual double Prices(); };
保护性数据 - 指标对象 CiCustom类型,并且在模块里根据描述符里指定的类型声明参数。
4.1.3. 构造器
构造器以省缺值初始化类的参数。之后, 根据 EA 的输入参数, 模块被包含进主要信号时, 这些值再次得到更新。
CPriceDeltaZZ::CPriceDeltaZZ() : m_app_price(1), m_rev_mode(0), m_pips(300), m_percent(0.5), m_levels(2), m_tp_ratio(1), m_exp_bars(10) { }
4.1.4. ValidationSettings()
ValidationSettings() 是检查输入参数的重要方法。如果模块参数值无效, 返回结果 false, 并且在日志中输出错误消息。
bool CPriceDeltaZZ::ValidationSettings(void) { //--- checking for settings of additional filters if(!CExpertSignal::ValidationSettings()) return(false); //--- initial data check if(m_app_price<0 || m_app_price>1) { printf(__FUNCTION__+": Applied price must be 0 or 1"); return(false); } if(m_rev_mode<0 || m_rev_mode>1) { printf(__FUNCTION__+": Reversal mode must be 0 or 1"); return(false); } if(m_pips<10) { printf(__FUNCTION__+": Number of pips in a ray must be at least 10"); return(false); } if(m_percent<=0) { printf(__FUNCTION__+": Percent must be greater than 0"); return(false); } if(m_levels<1) { printf(__FUNCTION__+": Ray Number must be at least 1"); return(false); } if(m_tp_ratio<=0) { printf(__FUNCTION__+": TP Ratio must be greater than zero"); return(false); } if(m_exp_bars<0) { printf(__FUNCTION__+": Expiration must be zero or positive value"); return(false); } //--- parameter check passed return(true); }
4.1.5. InitIndicators(...)
方法 InitIndicators(...) 调用基础类中的同名方法,并且通过 InitDeltaZZ(...) 方法初始化当前模块。
bool CPriceDeltaZZ::InitIndicators(CIndicators *indicators) { //--- initialization of indicator filters if(!CExpertSignal::InitIndicators(indicators)) return(false); //--- creating and initializing of custom indicator if(!InitDeltaZZ(indicators)) return(false); //--- success return(true); }
4.1.6. InitDeltaZZ(...)
方法 InitDeltaZZ(...) 添加定制指标对象到集合中,并创建新指标 "增量之字折线"。
bool CPriceDeltaZZ::InitDeltaZZ(CIndicators *indicators) { //--- adds to collection if(!indicators.Add(GetPointer(m_deltazz))) { printf(__FUNCTION__+": error adding object"); return(false); } //--- specifies indicator parameters MqlParam parameters[6]; //--- parameters[0].type=TYPE_STRING; parameters[0].string_value="deltazigzag.ex5"; parameters[1].type=TYPE_INT; parameters[1].integer_value=m_app_price; parameters[2].type=TYPE_INT; parameters[2].integer_value=m_rev_mode; parameters[3].type=TYPE_INT; parameters[3].integer_value=m_pips; parameters[4].type=TYPE_DOUBLE; parameters[4].double_value=m_percent; parameters[5].type=TYPE_INT; parameters[5].integer_value=m_levels; //--- object initialization if(!m_deltazz.Create(m_symbol.Name(),m_period,IND_CUSTOM,6,parameters)) { printf(__FUNCTION__+": error initializing object"); return(false); } //--- number of the indicator buffers if(!m_deltazz.NumBuffers(5)) return(false); //--- ок return(true); }
在方法 ValidationSettings(), InitDeltaZZ(...) 和 InitIndicators(...) 成功完成之后, 模块已初始化并准备准备工作。
4.1.7. Prices()
这个方法是价格模块的基础。此处是较好的更新订单参数的位置。它们的数值将传递给主要信号。方法返回操作 double 类型的结果。这主要是为实现未来的开发。方法 Prices() 的结果可以编写一些特殊情况和事件,从而主要信号可以相应地处理它们。目前只打算处理返回值 EMPTY_VALUE。一旦接收到该结果,主要信号将忽略由模块所建议的参数。
模块中方法 Prices() 的操作算法:
- 分别从指标的第3、第4缓存区获取买单和卖单的开仓价;
- 订单的最终参数归零;
- 检查买单价格。如果有一个, 通过 getBuySL() 方法识别放置止损的价位, 根据止损值计算止盈位和开仓价, 且计算订单过期时间;
- 检查卖单价。如果检测到, 通过 getSellSL() 方法找到放置止损的价位, 根据止损值计算止盈位和开仓价, 且计算订单过期时间;
- 退出。
double CPriceDeltaZZ::Prices(void) { double openbuy =m_deltazz.GetData(3,0);//receive the last value from buffer 3 double opensell=m_deltazz.GetData(4,0);//receive the last value from buffer 4 //--- clear parameter values m_order_open_long=EMPTY_VALUE; m_order_stop_long=EMPTY_VALUE; m_order_take_long=EMPTY_VALUE; m_order_expiration_long=0; m_order_open_short=EMPTY_VALUE; m_order_stop_short=EMPTY_VALUE; m_order_take_short=EMPTY_VALUE; m_order_expiration_short=0; int digits=m_symbol.Digits(); //--- check for the prices to buy if(openbuy>0)//if buffer 3 is not empty { m_order_open_long=NormalizeDouble(openbuy,digits); m_order_stop_long=NormalizeDouble(getBuySL(),digits); m_order_take_long=NormalizeDouble(m_order_open_long + m_tp_ratio*(m_order_open_long - m_order_stop_long),digits); m_order_expiration_long=calcExpiration(); } //--- check for the prices to sell if(opensell>0)//if buffer 4 is not empty { m_order_open_short=NormalizeDouble(opensell,digits); m_order_stop_short=NormalizeDouble(getSellSL(),digits); m_order_take_short=NormalizeDouble(m_order_open_short - m_tp_ratio*(m_order_stop_short - m_order_open_short),digits); m_order_expiration_short=calcExpiration(); } return(0); }
4.1.8. getBuySL() 和 getSellSL()
方法 getBuySL() 和 getSellSL() 寻找局部的最小和最大值作为止损位。在相应的缓存区里最后的非零值 – 每个方法里寻找的最后局部极值价位。
double CPriceDeltaZZ::getBuySL(void) { int i=0; double sl=0.0; while(sl==0.0) { sl=m_deltazz.GetData(0,i); i++; } return(sl); } double CPriceDeltaZZ::getSellSL(void) { int i=0; double sl=0.0; while(sl==0.0) { sl=m_deltazz.GetData(1,i); i++; } return(sl); }
4.2. 基于内柱线的价格模块
内柱线是一种在交易中广泛使用的模型或形态,无需指标,因此称作 价格行为。一个内柱线是一根柱线其最高和最低均在其前一根柱线的极值范围内。一个内柱线指示价格走势可能开始反转。
在图例. 4 内柱线以红色椭圆圈住。当检测到一根内柱线, 则价格模块根据所处理的内柱线的极值, 生成向上突破买和向下突破卖的订单开仓价。
开仓价是反向订单的止损位。止盈的计算与上面讨论的模块方式相同 - 以止损位乘以设置中指定的系数。开仓价和止损以红色水平线标记,而止盈价是绿色。
图例. 4. 价格模块的内柱线和价位描绘
在图例. 4 没有卖单,因为趋势上行。然而,价格模块产生两个方向的入场级别。一些止盈位超出了绘图窗口。
不像之前的模块,此模块的算法很简单,并且不需要指标初始化。还有就是模块中代码很少,下面呈现的就是它的全部内容。我们不打算拆开分析每个函数。
#include <Expert\ExpertSignalAdvanced.mqh> // wizard description start //+------------------------------------------------------------------+ //| Description of the class | //| Title=Inside Bar Price Module | //| Type=SignalAdvanced | //| Name=Inside Bar Price Module | //| ShortName=IB_PM | //| Class=CPriceInsideBar | //| Page=not used | //| Parameter=setTpRatio,double,2,TP:SL ratio | //| Parameter=setExpBars,int,10,Expiration after bars number | //| Parameter=setOrderOffset,int,5,Offset for open and stop loss | //+------------------------------------------------------------------+ // wizard description end //+------------------------------------------------------------------+ //| Class CPriceInsideBar | //| Purpose: Class of the generator of price levels for orders based | //| on the "inside bar" pattern. | //| Is derived from the CExpertSignalAdvanced class. | //+------------------------------------------------------------------+ class CPriceInsideBar : public CExpertSignalAdvanced { protected: double m_tp_ratio; //tp:sl ratio int m_exp_bars; //lifetime of the orders in bars double m_order_offset; //shift of the opening and Stop Loss levels datetime calcExpiration() { return(TimeCurrent()+m_exp_bars*PeriodSeconds(m_period)); } public: CPriceInsideBar(); ~CPriceInsideBar(); void setTpRatio(double ratio){ m_tp_ratio=ratio; } void setExpBars(int bars) { m_exp_bars=bars;} void setOrderOffset(int pips){ m_order_offset=m_symbol.Point()*pips;} bool ValidationSettings(); double Prices(); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CPriceInsideBar::CPriceInsideBar() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CPriceInsideBar::~CPriceInsideBar() { } //+------------------------------------------------------------------+ //| Validation of protected settings | //+------------------------------------------------------------------+ bool CPriceInsideBar::ValidationSettings(void) { //--- verification of the filter parameters if(!CExpertSignal::ValidationSettings()) return(false); //--- initial data check if(m_tp_ratio<=0) { printf(__FUNCTION__+": TP Ratio must be greater than zero"); return(false); } if(m_exp_bars<0) { printf(__FUNCTION__+": Expiration must be zero or positive value"); return(false); } //--- check passed return(true); } //+------------------------------------------------------------------+ //| Price levels refreshing | //+------------------------------------------------------------------+ double CPriceInsideBar::Prices(void) { double h[2],l[2]; if(CopyHigh(m_symbol.Name(),m_period,1,2,h)!=2 || CopyLow(m_symbol.Name(),m_period,1,2,l)!=2) return(EMPTY_VALUE); //--- check for inside bar if(h[0] >= h[1] && l[0] <= l[1]) { m_order_open_long=h[0]+m_order_offset; m_order_stop_long=l[0]-m_order_offset; m_order_take_long=m_order_open_long+(m_order_open_long-m_order_stop_long)*m_tp_ratio; m_order_expiration_long=calcExpiration(); m_order_open_short=m_order_stop_long; m_order_stop_short=m_order_open_long; m_order_take_short=m_order_open_short-(m_order_stop_short-m_order_open_short)*m_tp_ratio; m_order_expiration_short=m_order_expiration_long; return(0); } return(EMPTY_VALUE); } //+------------------------------------------------------------------+
在方法此模块的 Prices() 中, 返回 EMPTY_VALUE 结果来表示主要信号没有可用价位。
4.3. 基于外柱线的价格模块
外柱线是另一种流行的价格行为,也称为 "吞噬"。被称作外柱线是因为它的最高和最低与前一根柱线重叠。一根外柱线的出现就表明随后的针对性价格走势波动增加。
在图例. 5 外柱线以红色椭圆标记。一旦检测到一根外柱线, 价格模块在它的极值处生成向上突破买和向下突破卖的订单开仓价。
开仓价是反向订单的止损位。止盈的计算与上面讨论的模块方式相同 - 以止损位乘以设置中指定的系数。开仓价和止损以红色水平线标记,而止盈价是绿色。
图例. 5. 价格模块的外柱线和价位描绘
在图例. 5 外柱线以红色椭圆标记。水平线反映由价格模块生成的挂单开仓价。
在此情况下, 仅有卖单,因为趋势下行。此模块的操作基本上与前一个类似。它的代码仅有的不同在于 Prices() 方法, 其它部分是完全一样的,并具有相同的名称。以下是 Prices() 的代码。
double CPriceOutsideBar::Prices(void) { double h[2],l[2]; if(CopyHigh(m_symbol.Name(),m_period,1,2,h)!=2 || CopyLow(m_symbol.Name(),m_period,1,2,l)!=2) return(EMPTY_VALUE); //--- check of outside bar if(h[0] <= h[1] && l[0] >= l[1]) { m_order_open_long=h[1]+m_order_offset; m_order_stop_long=l[1]-m_order_offset; m_order_take_long=m_order_open_long+(m_order_open_long-m_order_stop_long)*m_tp_ratio; m_order_expiration_long=calcExpiration(); m_order_open_short=m_order_stop_long; m_order_stop_short=m_order_open_long; m_order_take_short=m_order_open_short-(m_order_stop_short-m_order_open_short)*m_tp_ratio; m_order_expiration_short=m_order_expiration_long; return(0); } return(EMPTY_VALUE); }
4.4. 模块性能测试
在下一节中,测试模块的三个例子来检查它们的独立性,还有在一款 EA 中联合工作,以及确认系统能够按照设计工作。作为结果, 生成四个 EA。基于动量振荡器指标的交易信号模块 在每款 EA 中作为过滤器工作在 H12 时间帧。价格模块工作在 H6。
资金管理: 固定手数和尾随终止未使用。所有测试均以 EURUSD 报价执行,在 MetaQuotes-Demo 服务器的模拟账户上,测试周期从 2013.01.01 至 2014.06.01。终端版本: 5.00, 编译号 965 (2014年6月27日)。
为了简化,测试结果仅表现为余额图。它们足以得到每个特定的情况下 EA 的行为。
4.4.1. 测试基于 "增量之字折线" 指标的模块
图例. 6. 基于"增量之字折线" 指标的价格模块的测试余额图
4.4.2. 测试基于 "内柱线" 形态的模块
图例. 7. 基于内柱线的价格模块的测试余额图
看图例. 7., 应该记住的是,在此阶段测试的目的是检查代码的性能,而不是得到盈利的策略。
4.4.3. 测试基于 "外柱线" 形态的模块
图例. 8. 基于外柱线的价格模块的测试余额图
4.4.4. 测试开发的价格模块的联合工作
考虑到以前的测试结果,基于 "增量之字折线",内柱线和外柱线的价格模块,分别获得了 1,0.5 和 0.7的权重系数。权重系数定义了价格模块的优先级。
图例. 9. 三种价格模块的测试余额图
在测试的每个阶段, 所有针对价格区间执行的操作均已仔细分析。没有引起策略错误和偏差。所有建议的程序附加到这篇文章,您可以自行进行测试。
5. 使用介绍
我们打算用带有三个价格模块和动量振荡器作为过滤器的 EA 作为样本扩展,来研究开发 EA 的阶段。
在起始阶段, 确认头文件 "CExpertAdvanced.mqh" 和 "CExpertSignalAdvanced.mqh" 已保存在 MetaTrader 5 终端的文件夹 MQL5/Include/Expert 并且价格模块文件也在文件夹 MQL5/Include/Expert/MySignal 里。在启动 EA 之前, 确保指标已经编译且位于 MQL5/Indicators 文件夹。在此情况下它的文件是 "DeltaZigZag.ex5"。在附带的存档里, 所有文件均处于它们应在的位置。所要做的就是将存档解压至 MetaTrader 5 终端的文件夹中并确认合并目录。
5.1. EA 生成
为了开始生成 EA, 在 MetaEditor 里选择 "文件"->"新文件"。
图例. 10. 使用 MQL5 向导创建新的 EA
在心窗口里选择 "智能程序 (生成)" 并按 "下一步"。
图例. 11. 使用 MQL5 想到生成 EA
它会弹出窗口,您可以指定名称。在这个特殊的例子里,EA 的名字是 "TEST_EA_AO_DZZ_IB_OB",参数 Symbol 和 TimeFrame 有省缺值。然后点击 "下一步"。
图例. 12. 交易程序的一般属性
在出现的窗口里逐一按下 "加入" 添加模块。整个过程表现如下。
请注意! 当您添加模块时, 首先包括所有信号模块 ( CExpertSignal 的继承者) 之后是价格模块 (CExpertSignalAdvanced 的继承者)。包含模块的顺序如果遭到破坏,结果将不可预料。
所以, 我们从动量振荡器开始包含信号模块。它是本例中仅有的一个过滤器。
图例. 13. 从动量振荡器包括信号模块
所有信号模块已被包进后,按随机顺序包含所需的价格模块。在本例中它们有三个。
图例. 14. 从 DeltaZZ 价格模块包含信号模块
图例. 15. 从内柱线价格模块包含信号模块
图例. 16. 从外柱线价格模块包含信号模块
所有模块已加入, 窗口如下所见:
图例. 17. 包含的交易信号模块列表
价格模块的 "权重" 参数值定义它的优先权。
在点击 "下一步" 按钮之后, 向导建议选择资金管理模块以及尾随停止模块。从中选择其一,或保留原设置, 按 "下一步" 或 "准备"。
按 "准备" 我们将得到 EA 的代码。
5.2. 编辑生成代码
所以, 我们已有代码。
1. 在开头查找字符串
#include <Expert\Expert.mqh>
并编辑它像这样
#include <Expert\ExpertAdvanced.mqh>
它需要包含一些开发的类文件。
2. 之后查找字符串
CExpert ExtExpert;
并改为
CExpertAdvanced ExtExpert;
它会按照所需功能改变 EA 的标准库及其后代。
3. 现在查找字符串
CExpertSignal *signal=new CExpertSignal;
并改为
CExpertSignalAdvanced *signal=new CExpertSignalAdvanced;
这种方式会按照所需功能改变主要信号的标准库及其后代。
4. 查找字符串,并在主要信号的 m_filters 数组里添加第一个价格模块。在此例中它看起来像这样:
signal.AddFilter(filter1);
在此之前我们插入字符串
signal.CalcPriceModuleIndex();
这是主要信号所需的,用来识别 m_filters 数组中指向的信号模块,以及价格模块,其数量可达索引数值。
找到插入指定字符串的正确位置可能会有点困难。在单词 "filter" 使用数字作为参照点。这将简化搜索,且不会错过正确的位置。MQL5 向导在代码中按照顺序自动命名包含的模块。第一个模块称为 filter0, 第二个 – filter1, 第三个 – filter2 等等。在我们的案例中仅有一个信号模块。所以, 第一个包含的价格模块是编号2,我们需要搜索字符串 "signal.AddFilter(filter1);" 为了添加过滤器,数码在代码里有前导 0。描绘如图例. 18:
图例. 18. 根据包含顺序,模块在代码里的命名
5. 这部分不是强制性的。通过引入变化,EA 参数负责开盘价缩进,止损,止盈,和订单过期弃用时间。为了使代码更紧凑,可以删除以下字符串:
input double Signal_PriceLevel =0.0; // Price level to execute a deal input double Signal_StopLevel =50.0; // Stop Loss level (in points) input double Signal_TakeLevel =50.0; // Take Profit level (in points) input int Signal_Expiration =4; // Expiration of pending orders (in bars)
删除上述字符串后,遇到编译错误,我们要删除下一组字符串:
signal.PriceLevel(Signal_PriceLevel); signal.StopLevel(Signal_StopLevel); signal.TakeLevel(Signal_TakeLevel); signal.Expiration(Signal_Expiration);
在后者被删除之后,编辑将会成功。
编辑注释和解释在 EA 附带的代码里可以找到 "TEST_EA_AO_DZZ_IB_OB"。代码字符串以及随同的注释可以删除。
结论
在本文中,我们显著扩展了 MQL5 向导的应用领域。现在它可以用来开发优化的,无论当前价位所在,可以在不同价位放置订单,止损和止盈的自动交易系统。
生成的 EA 可以包括一组价格模块,计算订单发送的参数。从可用的集合里面选择最合适的参数组。设置中指定的为首选。它允许使用多个不同的,最高效率的入场点。这种方法使得 EA 具有选择性。如果方向已知,但入场点不确定,那么 EA 会等待它出现。
引入标准兼容模块负责搜索价位有显著优点,其目的是简化 EA 开发。虽然此刻只有三个模块,它们的数量在未来无疑将会增加。如果您发现本文十分有用,请在评论栏建议价格模块操作的算法。有趣的想法将在代码里实现。
本文还提供了在向导中进一步开发扩展 EA 能力的一种方法。继承是引入变化的最佳方式。
没有编程经验的用户不光能够根据指导编辑代码, 还有机会创建基于可用模型的,更加先进的 EA。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/987