当开发交易算法时,我们几乎总是会遇到一个难题:如何确定趋势/盘整在哪里开始和结束? 很难找到一个完美的解决方案。这个目标看起来可以通过把基于趋势和基于盘整的策略组合为一个算法来实现。在本文中,我们将会创建一个通用的指标,它将组合来自不同类型策略的信号。我们将会尝试在 EA 中尽可能简化交易信号的生成,并将给出一个把几个指标组合为一的实例。这可以通过使用面向对象编程来做到,每个指标或者它的一部分以类的形式包含在主程序文件中。
所以,这里的任务是写一个 EA 交易来组合两个交易策略:一个用于趋势交易,另一个用于盘整时期。我们假定,同时双向交易更加高效,并且这样的策略将更加稳定,那么在一个EA交易中只包含一个指标将更加方便,而这个指标将生成两种策略类型的信号。在其中,您可以实现一个复杂的系统,用于判断买入和卖出信号。可能需要在其中组合几个使用不同设置的相同指标成为一体,或者在指标中包含两个不同的:主指标和过滤指标(辅助)。使用OOP(面向对象编程)来实现这样的架构非常方便。
下面的框架显示了在主指标文件(Indicator.mq5)中包含两个指标类 (CIndicator1 和 CIndicator2),CIndicator2 是一个辅助指标,它的计算结果是 CIndicator1 所需要的。在此,我们使用了之前在 关于创建多交易品种指标的文章中确定真实柱形的方法,为这篇文章我们写了一个独立的 CFirstTrueBar 类。它将包含于所有的指标中,以避免在不属于当前时段的柱上进行计算。
图 1. 一个可行的使用 OOP 创建指标的框架
任何来自于标准终端分发包的指标都可以选择用来生成信号,它们中的大部分都有一个类似的思路,作为规则,它们中的任何一个都不优先于其它的指标。一些指标与过滤器的组合在某些时间段有效,而其他的可能在不同的时间段有效,
但是为了研究方便,最好是选择振荡器类型的指标,它们在有趋势和盘整的时候都可以用于确定信号。振荡器的数据也可以用于绘制价格通道,这样,我们就可以创建一个通用指标,在开发任何复杂度的交易策略时都很方便。
在本系列文章中,我们将使用 RSI (相对强弱指数,Relative Strength Index) 指标作为实例。下面是这个指标在周期数为8的 AUDUSD H1 图表上计算的结果。
图 2. 相对强弱指数指标
最初看来,很容易根据这个指标的信号来获取利润,但这是一种错觉。在你达到下一步的大致理解之前,需要做大量的工作。同时,还无法保证能够达到你的目标。
考虑最简单和明显的例子: 我们相信指标线在与默认水平交叉的时候会有利润: 70/30. 如果向下与 70 水平线交叉,它是个卖出信号。如果向上与 30 水平线交叉,它是个买入信号。但是,我们看到很多错误信号,价格走向了持仓相反的方向。
这里是另一个使用这个指标分析信号的例子 (参见图3). 我们看到,价格下跌了很长时间,红色线标记了指标向上与30水平交叉所构成的信号,如果您的算法包含了根据这些信号来买入,您将会得到浮动的回撤,如果您每次都设置了止损,就会产生好几次亏损。同时,从最后一次买入信号到出现卖出信号(绿色线)价格都没有达到正的结果,您的结果就是亏损。另外,这段图表会建议跟随趋势进行交易,也就表示我们没有看到任何不确定的因素。
当使用任何指标时都会遇到类似的问题,所以,将来我们选择什么指标并没有关系。
图 3. RSI 指标的信号
应该在选定的指标中加入功能,使它能更加方便地在 EA 交易中使用,我们创建了5个版本的 RSI,按顺序从简单到复杂(以便容易理解)。
标准版本的 RSI 位于终端的 \MQL5\Indicators\Examples 目录下,让我们把它复制一份,再开始修改它。我们在固定的参数列表上加上了另外两个指标缓冲区,它们的总数将也等于 5, 和 3,将显示在图表上。两个缓冲区将为辅助计算保留,买入信号的标签将使用绿色 (clrMediumSeaGreen), 而卖出信号使用红色 (clrRed)。
//--- 属性 #property indicator_separate_window #property indicator_minimum 0 #property indicator_maximum 100 #property indicator_buffers 5 #property indicator_plots 3 #property indicator_color1 clrSteelBlue #property indicator_color2 clrMediumSeaGreen #property indicator_color3 clrRed
定义用于信号标签的代码。如果需要以点显示,代码为 159,如果信号使用箭头来显示,使用的代码分别是 233 和 234。
//--- 信号箭头: 159 - 点; 233/234 - 箭头 #define ARROW_BUY 159 #define ARROW_SELL 159
指标线与边界的交叉也可以既作为买入也作为卖出信号,所以,外部参数将需要一个枚举,它可以用来指定如何解释指标信号。
所有这些模式将显示在下面的图表中。
//--- 用于突破通道边界模式的枚举 enum ENUM_BREAK_INOUT { BREAK_IN =0, // Break in(向内突破模式) BREAK_IN_REVERSE =1, // Break in reverse(向内突破反转信号模式) BREAK_OUT =2, // Break out(向外突破模式) BREAK_OUT_REVERSE =3 // Break out reverse(向外突破反转信号模式) };
指标将一共有三个外部参数:
//--- 输入参数 input int PeriodRSI =8; // RSI 周期数 input double SignalLevel =30; // 信号水平 input ENUM_BREAK_INOUT BreakMode =BREAK_OUT; // 突破模式
指标的属性是在 SetPropertiesIndicator() 函数中设置的,最后会设置辅助数组,所有的指标数组在 ZeroIndicatorBuffers() 函数中使用0来做初始化,然后, 我们指定0值不应当显示在图表上, 意思是这样的数值是空的。
//+------------------------------------------------------------------+ //| 设置指标属性 | //+------------------------------------------------------------------+ void SetPropertiesIndicator(void) { //--- 短名称 ::IndicatorSetString(INDICATOR_SHORTNAME,"RSI_PLUS1"); //--- 小数点位置 ::IndicatorSetInteger(INDICATOR_DIGITS,2); //--- 指标数组 ::SetIndexBuffer(0,rsi_buffer,INDICATOR_DATA); ::SetIndexBuffer(1,buy_buffer,INDICATOR_DATA); ::SetIndexBuffer(2,sell_buffer,INDICATOR_DATA); ::SetIndexBuffer(3,pos_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(4,neg_buffer,INDICATOR_CALCULATIONS); //--- 初始化数组 ZeroIndicatorBuffers(); //--- 设置文字标签 string plot_label[]={"RSI","buy","sell"}; for(int i=0; i<indicator_plots; i++) ::PlotIndexSetString(i,PLOT_LABEL,plot_label[i]); //--- 设置指标数组的宽度 for(int i=0; i<indicator_plots; i++) ::PlotIndexSetInteger(i,PLOT_LINE_WIDTH,1); //--- 设置指标数组的类型 ENUM_DRAW_TYPE draw_type[]={DRAW_LINE,DRAW_ARROW,DRAW_ARROW}; for(int i=0; i<indicator_plots; i++) ::PlotIndexSetInteger(i,PLOT_DRAW_TYPE,draw_type[i]); //--- 标签代码 ::PlotIndexSetInteger(1,PLOT_ARROW,ARROW_BUY); ::PlotIndexSetInteger(2,PLOT_ARROW,ARROW_SELL); //--- 开始计算的起始元件的索引 for(int i=0; i<indicator_plots; i++) ::PlotIndexSetInteger(i,PLOT_DRAW_BEGIN,period_rsi); //--- 指标水平线的数量 ::IndicatorSetInteger(INDICATOR_LEVELS,2); //--- 指标水平线的值 up_level =100-SignalLevel; down_level =SignalLevel; ::IndicatorSetDouble(INDICATOR_LEVELVALUE,0,down_level); ::IndicatorSetDouble(INDICATOR_LEVELVALUE,1,up_level); //--- 线型 ::IndicatorSetInteger(INDICATOR_LEVELSTYLE,0,STYLE_DOT); ::IndicatorSetInteger(INDICATOR_LEVELSTYLE,1,STYLE_DOT); //--- 表示不画任何内容的空值 for(int i=0; i<indicator_buffers; i++) ::PlotIndexSetDouble(i,PLOT_EMPTY_VALUE,0); //--- 在 Y 轴上的偏移 if(BreakMode==BREAK_IN_REVERSE || BreakMode==BREAK_OUT_REVERSE) { ::PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,arrow_shift); ::PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,-arrow_shift); } else { ::PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,-arrow_shift); ::PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,arrow_shift); } }
为方便起见,RSI 指标值的预先计算和主体计算部分移动到了独立的函数 PreliminaryCalculations() 和 CalculateRSI() 中,它们的内容和标准分发包中的 RSI 指标是一样的让我们只探讨用于确定指标信号的函数 — CalculateSignals(),在这里,首先会根据外部参数中设置的模式来检查条件,然后,如果条件满足,就把 RSI 指标值保存到对应的指标数组中。如果条件不满足,就保存0值,它们将不会显示在图表中。
//+------------------------------------------------------------------+ //| 计算指标信号 | //+------------------------------------------------------------------+ void CalculateSignals(const int i) { bool condition1 =false; bool condition2 =false; //--- 突破进入通道 if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE) { condition1 =rsi_buffer[i-1]<down_level && rsi_buffer[i]>down_level; condition2 =rsi_buffer[i-1]>up_level && rsi_buffer[i]<up_level; } else { condition1 =rsi_buffer[i-1]<up_level && rsi_buffer[i]>up_level; condition2 =rsi_buffer[i-1]>down_level && rsi_buffer[i]<down_level; } //--- 如果条件满足,显示信号 if(BreakMode==BREAK_IN || BreakMode==BREAK_OUT) { buy_buffer[i] =(condition1)? rsi_buffer[i] : 0; sell_buffer[i] =(condition2)? rsi_buffer[i] : 0; } else { buy_buffer[i] =(condition2)? rsi_buffer[i] : 0; sell_buffer[i] =(condition1)? rsi_buffer[i] : 0; } }
这样,主指标函数的代码,例如 OnInit() 和 OnCalculate(), 就变得清晰易读了:
//+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ void OnInit(void) { //--- 检查外部参数值 if(PeriodRSI<1) { period_rsi=2; Print("输入变量值不正确, PeriodRSI =",PeriodRSI, "指标将会使用数值 =",period_rsi," 来进行计算."); } else period_rsi=PeriodRSI; //--- 设置指标属性 SetPropertiesIndicator(); } //+------------------------------------------------------------------+ //| 相对强弱指数 | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- 如果数据量不够就退出 if(rates_total<=period_rsi) return(0); //--- 预备计算 PreliminaryCalculations(prev_calculated,close); //--- 主计算循环 for(int i=start_pos; i<rates_total && !::IsStopped(); i++) { //--- 计算 RSI 指标 CalculateRSI(i,close); //--- 计算信号 CalculateSignals(i); } //--- 返回最近计算的元件数量 return(rates_total); }
把指标附加到图表上,看看会发生什么。下面是四种运行模式 (外部参数 Break Mode) 的结果.
图 4. 演示修改版 RSI 指标的运行。
在这样修改了 RSI 指标之后,它所生成的错误信号的数量看起来好转了。分析了图形之后,我们可以下结论,这个指标有的时候在盘整的时候效果更好,有的时候在有趋势的时候更好。
这个信号能够只在盘整时或者只在有趋势的时候进行交易吗?如何才能提高获利的概率和入场的准确性呢?很有可能如果仓位不是根据连续序列中的第一个信号所开启,则可能会提升结果。
让我们尝试增加指标缓冲区,其中将显示在一个方向上连续的信号数量。一旦反向信号触线,之前缓冲区的计数器会被清零,而当前序列的信号计数器将被激活。为了实现它,我们稍微增加了一些代码,
在特定的参数中也有些改变,指定了新的缓冲区数量,用于绘制的序列以及为它们设置颜色。
//--- 属性 ... #property indicator_buffers 7 #property indicator_plots 5 ... #property indicator_color4 clrMediumSeaGreen #property indicator_color5 clrRed
在全局范围中,增加了另外两个数组用来显示计数器的数值,以及两个辅助变量:
//--- 指标数组 ... double buy_counter_buffer[]; double sell_counter_buffer[]; //--- 连续信号序列的计数器 int buy_counter =0; int sell_counter =0;
剩下的修改只有关于确定信号的函数,在此,如果条件满足 (出现了下一个买入或者卖出信号), 对应的计数器会被触发,而反向信号的计数器要被重置。为了避免在当前柱上增加计数器, 需要跳过在最新的未完成柱上的触发。
//+------------------------------------------------------------------+ //| 计算指标信号 | //+------------------------------------------------------------------+ void CalculateSignals(const int i,const int rates_total) { int last_index=rates_total-1; //--- bool condition1 =false; bool condition2 =false; //--- 突破进入通道 if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE) { condition1 =rsi_buffer[i-1]<down_level && rsi_buffer[i]>down_level; condition2 =rsi_buffer[i-1]>up_level && rsi_buffer[i]<up_level; } else { condition1 =rsi_buffer[i-1]<up_level && rsi_buffer[i]>up_level; condition2 =rsi_buffer[i-1]>down_level && rsi_buffer[i]<down_level; } //--- 如果条件满足,显示信号 if(BreakMode==BREAK_IN || BreakMode==BREAK_OUT) { buy_buffer[i] =(condition1)? rsi_buffer[i] : 0; sell_buffer[i] =(condition2)? rsi_buffer[i] : 0; //--- 只在完全生成的柱上处理计数器 if(i<last_index) { if(condition1) { buy_counter++; sell_counter=0; } else if(condition2) { sell_counter++; buy_counter=0; } } } else { buy_buffer[i] =(condition2)? rsi_buffer[i] : 0; sell_buffer[i] =(condition1)? rsi_buffer[i] : 0; //--- 只在完全生成的柱上处理计数器 if(i<last_index) { if(condition2) { buy_counter++; sell_counter=0; } else if(condition1) { sell_counter++; buy_counter=0; } } } //--- 修正最近的数值 (等于最终的) if(i<last_index) { buy_counter_buffer[i] =buy_counter; sell_counter_buffer[i] =sell_counter; } else { buy_counter_buffer[i] =buy_counter_buffer[i-1]; sell_counter_buffer[i] =sell_counter_buffer[i-1]; } }
在修改完成之后,把指标附加到图表上看看会发生什么,现在 RSI 指标包含了更多的信息,这些计数器的值可以在 EA 交易中取得,以生成用于买入和卖出的条件。
图 5. 修改过的带有相同方向上连续信号计数器的 RSI 指标
为了使一切按照计划进行,我们在测试器和实时中都检查了这个指标。在策略测试器中的结果:
图 6. 在策略测试器中检查修改的 RSI 指标的运行.
下面的屏幕截图显示的是在使用所有的 Break Mode 参数的指标值。
图 7. Break Mode 参数中所有的运行模式下的指标。
在图表上可能会有一种情况,价格可能会在两个相同信号之间有很大的距离,这可能会经常出现,并且出现在任何时段中同时,您可以修改 Signal Level 外部参数为任何值,配置范围的边界 - 这也不会解决这个问题。这种不确定性可能会影响到清晰交易逻辑的创建或者需要使用另外的条件使它变得更加复杂。
图 8. 在很大价格变化的时候跳过信号.
在下一个版本中,我们将会消除这样的跳空,使指标提供更多的信息。
在这个版本中,指标缓冲区的数量保持不变,但是需要增加指标水平的数组来在指标与水平线交叉的时候生成信号。
//--- 指标水平线的值和它们的数量 double up_level =0; double down_level =0; int up_levels_total =0; int down_levels_total =0; //--- 水平线的数组 double up_levels[]; double down_levels[];
为了避免跳过信号,就像前一部分所说的,每隔 5 个点就设置水平线。也就是说,如果在 Signal Level 外部参数中设置了数值 30,则会计算下面的顶部水平: 70, 75, 80, 85, 90, 95.
这些指标水平是使用 GetLevelsIndicator() 函数来计算的。要在两个独立的循环中计算这些水平值,放到数组中。这个函数会返回水平线的总数。
//+------------------------------------------------------------------+ //| 返回指标水平线 | //+------------------------------------------------------------------+ int GetLevelsIndicator(void) { int levels_counter=0; double level=down_level; //--- 低于底部边界的底部水平线 while(level>0 && !::IsStopped()) { int size=::ArraySize(down_levels); ::ArrayResize(down_levels,size+1); down_levels[size]=level; level-=5; levels_counter++; } level=up_level; //--- 高于顶部边界的顶部水平线 while(level<100 && !::IsStopped()) { int size=::ArraySize(up_levels); ::ArrayResize(up_levels,size+1); up_levels[size]=level; level+=5; levels_counter++; } //--- return(levels_counter); }
水平线是在 SetPropertiesIndicator() 函数中设置的,下面显示的是它的精简版本。在这里,首先会计算顶部和底部范围的初始水平,而水平数组会被清零。然后 通过调用 GetLevelsIndicator() 函数来设置指标水平线的总数。随后,计算的顶部和底部范围中的水平线从数组中设置。
//+------------------------------------------------------------------+ //| 设置指标属性 | //+------------------------------------------------------------------+ void SetPropertiesIndicator(void) { ... //--- 计算第一个水平线 up_level =100-SignalLevel; down_level =SignalLevel; //--- 水平线数组清零 ::ArrayFree(up_levels); ::ArrayFree(down_levels); //--- 指标水平线的数量 ::IndicatorSetInteger(INDICATOR_LEVELS,GetLevelsIndicator()); //--- 底部水平的指标水平线数值 down_levels_total=::ArraySize(down_levels); for(int i=0; i<down_levels_total; i++) ::IndicatorSetDouble(INDICATOR_LEVELVALUE,i,down_levels[i]); //--- 顶部水平的指标水平线数值 up_levels_total=::ArraySize(up_levels); int total=up_levels_total+down_levels_total; for(int i=down_levels_total,k=0; i<total; i++,k++) ::IndicatorSetDouble(INDICATOR_LEVELVALUE,i,up_levels[k]); ... }
还需要相应地修改 CalculateSignals() 函数,这里也是只显示了函数中的修改部分。为了在循环中检查条件是否满足,看是否与数组中的水平线至少有一个交叉。
//+------------------------------------------------------------------+ //| 计算指标信号 | //+------------------------------------------------------------------+ void CalculateSignals(const int i,const int rates_total) { int last_index=rates_total-1; //--- bool condition1 =false; bool condition2 =false; //--- 突破进入通道 if(BreakMode==BREAK_IN || BreakMode==BREAK_IN_REVERSE) { if(rsi_buffer[i]<50) { for(int j=0; j<down_levels_total; j++) { condition1=rsi_buffer[i-1]<down_levels[j] && rsi_buffer[i]>down_levels[j]; if(condition1) break; } } //--- if(rsi_buffer[i]>50) { for(int j=0; j<up_levels_total; j++) { condition2=rsi_buffer[i-1]>up_levels[j] && rsi_buffer[i]<up_levels[j]; if(condition2) break; } } } else { for(int j=0; j<up_levels_total; j++) { condition1=rsi_buffer[i-1]<up_levels[j] && rsi_buffer[i]>up_levels[j]; if(condition1) break; } //--- for(int j=0; j<down_levels_total; j++) { condition2=rsi_buffer[i-1]>down_levels[j] && rsi_buffer[i]<down_levels[j]; if(condition2) break; } } //--- 如果条件满足,显示信号 ... //--- 修正最近的数值 (等于最终的) ... }
图9显示了它看起来的样子。
图 9. 当穿过多条水平线时生成信号。
一个问题解决了,但是又多出来两个问题。第一个是需要在价格比之前买入信号价格更高的时候以及价格比之前卖出信号价格更低的时候要排除信号。图 10 演示了对于一系列买入信号的这种情况:最近信号处的价格高于之前信号处的价格,这是与 Break in 和 Break out reverse 模式相关的,要坚持逢低买入、逢高卖出的概念。
图 10. 信号处的价格高于之前信号处的价格
第二个问题是信号太频繁了,有的时候甚至几个柱都连成一行了,在这种情况下,价格是在信号间只有一点距离。
图 11. 频繁的信号在小价格波动中聚集
这些问题将在下一个版本中得到解决。
这个版本的指标将被移动到主图表中,这个指标的运行看起来更好:信号直接显示在价格处。为了控制信号之间的距离(点数),可以在外部参数中简单设置一个固定值,但是我们将会尝试创建一个动态变量,并且把这个值与波动性指标 (ATR) 绑定起来。为了更加方便,指标的计算将写在独立的类中: CATR 和 CRsiPlus。可以使用这种方法组合任意数量的指标,在一个程序中组合它们的计算结果。
假定这个版本是用来在将来开发 EA 交易的,所以,为了消除在历史数据中更高时段数据的影响,在当前时段的柱形不够的时候我们将会确定真实的柱形。这已经在关于多交易品种指标的文章中详细介绍过了。为乐确定第一个真实的柱,我们写了一个独立的类 CFirstTrueBar。首先,让我们仔细看看这个类,
CFirstTrueBar 类的成员和方法在下面显示,让我们简要看一下它们。
//+------------------------------------------------------------------+ //| 用于确定真实柱形的类 | //+------------------------------------------------------------------+ class CFirstTrueBar { private: //--- 真实柱形的时间 datetime m_limit_time; //--- 真实柱形的编号 int m_limit_bar; //--- public: CFirstTrueBar(void); ~CFirstTrueBar(void); //--- 返回真实柱形的 (1) 时间 以及 (2) 编号 datetime LimitTime(void) const { return(m_limit_time); } int LimitBar(void) const { return(m_limit_bar); } //--- 确定第一个真实柱形 bool DetermineFirstTrueBar(void); //--- private: //--- 搜索当前时段的第一个真实柱形 void GetFirstTrueBarTime(const datetime &time[]); };
私有方法 CFirstTrueBar::GetFirstTrueBarTime() 是用于搜索真实柱形的,应当向它传递一个历史柱的时间数组来搜索第一个真实的柱。从数组的起点开始迭代,当找到一个对应当前时段的柱时, 就保存时间和这个柱的索引。当真实的柱形被标识之后,它的时间和索引就可以通过使用 CFirstTrueBar::LimitTime() 和 CFirstTrueBar::LimitBar() 方法来获得。
//+------------------------------------------------------------------+ //| 搜索当前时间段的第一个真实柱形 | //+------------------------------------------------------------------+ void CFirstTrueBar::GetFirstTrueBarTime(const datetime &time[]) { //--- 取得数组的大小 int array_size=::ArraySize(time); ::ArraySetAsSeries(time,false); //--- 挨个检查每个柱 for(int i=1; i<array_size; i++) { //--- 如果柱的时间对应了当前的时段 if(time[i]-time[i-1]==::PeriodSeconds()) { //--- 保存它并终止循环 m_limit_time =time[i-1]; m_limit_bar =i-1; break; } } }
CFirstTrueBar::GetFirstTrueBarTime() 方法是在 CFirstTrueBar::DetermineFirstTrueBar() 方法中调用的,这就是取得柱时间数组的地方, 晚些时候它会用于搜索第一个真实柱形。
//+------------------------------------------------------------------+ //| 确定第一个真实的柱形 | //+------------------------------------------------------------------+ bool CFirstTrueBar::DetermineFirstTrueBar(void) { //--- 柱时间的数组 datetime time[]; //--- 取得当前交易品种的总的柱数 int available_bars=::Bars(_Symbol,_Period); //--- 复制柱的时间数组,如果这个操作失败,就再次试试 if(::CopyTime(_Symbol,_Period,0,available_bars,time)<available_bars) return(false); //--- 取得第一个真实柱形对应当前时段的时间 GetFirstTrueBarTime(time); return(true); }
ATR 指标将按照标准分发包中的相同方法进行计算,代码是从这里取得的: \MQL5\Indicators\Examples. 下面是 CATR 类中成员和方法的声明,与标准方法的唯一区别就是第一个真实柱形确定的起点是计算开始的地方。
包含 CFirstTrueBar 类的文件,并在 CATR 类的内部声明它的一个实例,请注意, 这里的指标数组定义为可以公有访问,这是必要的,因为会在主指标文件中把它们设为指标缓冲区。
//+------------------------------------------------------------------+ //| ATR.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "FirstTrueBar.mqh" //+------------------------------------------------------------------+ //| ATR 指标 | //+------------------------------------------------------------------+ class CATR { private: //--- 确定第一个真实柱形 CFirstTrueBar m_first_true_bar; //--- 指标周期数 int m_period; //--- 计算指标值的限制 int m_limit; //--- public: //--- 指标缓冲区 double m_tr_buffer[]; double m_atr_buffer[]; //--- public: CATR(const int period); ~CATR(void); //--- 指标周期数 void PeriodATR(const int period) { m_period=period; } //--- 计算 ATR 指标 bool CalculateIndicatorATR(const int rates_total,const int prev_calculated,const datetime &time[],const double &close[],const double &high[],const double &low[]); //--- 指标缓冲区清零 void ZeroIndicatorBuffers(void); //--- private: //--- 预先计算 bool PreliminaryCalc(const int rates_total,const int prev_calculated,const double &close[],const double &high[],const double &low[]); //--- 计算 ATR void CalculateATR(const int i,const datetime &time[],const double &close[],const double &high[],const double &low[]); };
在预先计算方法中,会确定第一个真实柱形, 指标会在将来从这里开始计算。如果没有确定,程序就会退出这个方法。
//+------------------------------------------------------------------+ //| 预先计算 | //+------------------------------------------------------------------+ bool CATR::PreliminaryCalc(const int rates_total,const int prev_calculated,const double &close[],const double &high[],const double &low[]) { //--- 是第一次计算还是有了变化 if(prev_calculated==0) { //--- 确定真实柱形的编号 m_first_true_bar.DetermineFirstTrueBar(); //--- 如果真实柱形没有确定,就退出 if(m_first_true_bar.LimitBar()<0) return(false); //--- m_tr_buffer[0] =0.0; m_atr_buffer[0] =0.0; //--- 开始计算的柱形 m_limit=(::Period()<PERIOD_D1)? m_first_true_bar.LimitBar()+m_period : m_period; //--- 如果超出范围(柱数不够), 就退出 if(m_limit>=rates_total) return(false); //--- 计算真实范围的值 int start_pos=(m_first_true_bar.LimitBar()<1)? 1 : m_first_true_bar.LimitBar(); for(int i=start_pos; i<m_limit && !::IsStopped(); i++) m_tr_buffer[i]=::fmax(high[i],close[i-1])-::fmin(low[i],close[i-1]); //--- 第一个 ATR 值还没有计算 double first_value=0.0; for(int i=m_first_true_bar.LimitBar(); i<m_limit; i++) { m_atr_buffer[i]=0.0; first_value+=m_tr_buffer[i]; } //--- 第一个值的计算 first_value/=m_period; m_atr_buffer[m_limit-1]=first_value; } else m_limit=prev_calculated-1; //--- return(true); }
除了上面所描述的指标缓冲区,这个版本的 RSI 指标将又会多两个缓冲区。它们将是根据买入和卖出信号时的价格画出的连续水平线,同时考虑到了点差。为了能够在计算中包含 ATR 指标的数据,需要取得在主程序文件中创建的 ATR 类实例的指针,所以,要在这里声明一个 CATR 类型的指针,以及设置和读取它的方法。
为了优化代码,一些部分现在是以独立方法来实现了。这些包含了检查条件,操作计数器,等等。唯一新增的、之前没有讨论过的就是 CRsiPlus::DirectionControl(),它是控制移动方向的,并且会根据当前的波动性来过滤掉过多的信号,另外,还有些辅助方法用来删除不需要的信号 — CRsiPlus::DeleteBuySignal() 和 CRsiPlus::DeleteSellSignal()。
//+------------------------------------------------------------------+ //| RSI.mqh | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "FirstTrueBar.mqh" #include "ATR.mqh" //--- 通道突破模式的枚举 enum ENUM_BREAK_INOUT { BREAK_IN =0, // Break in(向内突破模式) BREAK_IN_REVERSE =1, // Break in reverse(向内突破反转信号模式) BREAK_OUT =2, // Break out(向外突破模式) BREAK_OUT_REVERSE =3 // Break out reverse(向外突破反转信号模式) }; //+------------------------------------------------------------------+ //| 带有波动过滤器的 RSI 指标 | //+------------------------------------------------------------------+ class CRsiPlus { private: //--- 确定第一个真实柱形 CFirstTrueBar m_first_true_bar; //--- ATR 的指针 CATR *m_atr; //--- 指标周期数 int m_period; //--- RSI 水平 double m_signal_level; //--- 用于生成信号的模式 ENUM_BREAK_INOUT m_break_mode; //--- 相同方向上的信号计数器 int m_buy_counter; int m_sell_counter; //--- 指标水平 double m_up_level; double m_down_level; double m_up_levels[]; double m_down_levels[]; int m_up_levels_total; int m_down_levels_total; //--- 计算指标值的限制 int m_limit; //--- 用于确定最近的柱形 bool m_is_last_index; //--- public: //--- 指标缓冲区 double m_rsi_buffer[]; double m_pos_buffer[]; double m_neg_buffer[]; //--- double m_buy_buffer[]; double m_sell_buffer[]; double m_buy_level_buffer[]; double m_sell_level_buffer[]; double m_buy_counter_buffer[]; double m_sell_counter_buffer[]; //--- public: CRsiPlus(const int period,const double signal_level,const ENUM_BREAK_INOUT break_mode); ~CRsiPlus(void) {} //--- ATR 的指针 void AtrPointer(CATR &object) { m_atr=::GetPointer(object); } CATR *AtrPointer(void) { return(::GetPointer(m_atr)); } //--- 计算 RSI 指标 bool CalculateIndicatorRSI(const int rates_total,const int prev_calculated,const double &close[],const int &spread[]); //--- 初始化指标缓冲区 void ZeroIndicatorBuffers(void); //--- private: //--- 取得指标水平线 int GetLevelsIndicator(void); //--- 预先计算 bool PreliminaryCalc(const int rates_total,const int prev_calculated,const double &close[]); //--- 计算 RSI 序列 void CalculateRSI(const int i,const double &price[]); //--- 计算指标信号 void CalculateSignals(const int i,const int rates_total,const double &close[],const int &spread[]); //--- 检查条件 void CheckConditions(const int i,bool &condition1,bool &condition2); //--- 检查计数器 void CheckCounters(bool &condition1,bool &condition2); //--- 增加买入和卖出信号计数器 void IncreaseBuyCounter(const bool condition); void IncreaseSellCounter(const bool condition); //--- 控制移动方向 void DirectionControl(const int i,bool &condition1,bool &condition2); //--- 删除多余的买入和卖出信号 void DeleteBuySignal(const int i); void DeleteSellSignal(const int i); //--- 清空指标缓冲区中的指定元素 void ZeroIndexBuffers(const int index); };
CRsiPlus::DirectionControl() 方法会检查以下条件,它们决定着信号是否多余:
如果符合这样的条件,信号就会被删除。
//+------------------------------------------------------------------+ //| 控制移动方向 | //+------------------------------------------------------------------+ void CRsiPlus::DirectionControl(const int i,bool &condition1,bool &condition2) { double atr_coeff =0.0; double impulse_size =0.0; bool atr_condition =false; //--- bool buy_condition =false; bool sell_condition =false; //--- 如果禁用了反转 if(m_break_mode==BREAK_IN || m_break_mode==BREAK_OUT) { buy_condition =condition1 && m_buy_counter>1; impulse_size =::fabs(m_buy_buffer[i]-m_buy_level_buffer[i-1]); atr_condition =impulse_size<m_atr.m_atr_buffer[i]; //--- if((m_buy_counter>1 && atr_condition) || (m_break_mode==BREAK_IN && buy_condition && m_buy_buffer[i]>m_buy_level_buffer[i-1]) || (m_break_mode==BREAK_OUT && buy_condition && m_buy_buffer[i]<m_buy_level_buffer[i-1])) { DeleteBuySignal(i); } //--- sell_condition =condition2 && m_sell_counter>1; impulse_size =::fabs(m_sell_buffer[i]-m_sell_level_buffer[i-1]); atr_condition =impulse_size<m_atr.m_atr_buffer[i]; //--- if((m_sell_counter>1 && atr_condition) || (m_break_mode==BREAK_IN && sell_condition && m_sell_buffer[i]<m_sell_level_buffer[i-1]) || (m_break_mode==BREAK_OUT && sell_condition && m_sell_buffer[i]>m_sell_level_buffer[i-1])) { DeleteSellSignal(i); } } //--- 启用了反转模式 else { buy_condition =condition2 && m_buy_counter>1; impulse_size =::fabs(m_buy_buffer[i]-m_buy_level_buffer[i-1]); atr_condition =impulse_size<m_atr.m_atr_buffer[i]; //--- if((m_buy_counter>1 && atr_condition) || (m_break_mode==BREAK_IN_REVERSE && buy_condition && m_buy_buffer[i]<m_buy_level_buffer[i-1]) || (m_break_mode==BREAK_OUT_REVERSE && buy_condition && m_buy_buffer[i]>m_buy_level_buffer[i-1])) { DeleteBuySignal(i); } //--- sell_condition =condition1 && m_sell_counter>1; impulse_size =::fabs(m_sell_buffer[i]-m_sell_level_buffer[i-1]); atr_condition =impulse_size<m_atr.m_atr_buffer[i]; //--- if((m_sell_counter>1 && atr_condition) || (m_break_mode==BREAK_IN_REVERSE && sell_condition && m_sell_buffer[i]>m_sell_level_buffer[i-1]) || (m_break_mode==BREAK_OUT_REVERSE && sell_condition && m_sell_buffer[i]<m_sell_level_buffer[i-1])) { DeleteSellSignal(i); } } }
现在让我们仔细看看指标的主文件,这个版本的指标已经有了 11 个缓冲区, 其中有7个主缓冲区,, 还有 4 个是辅助缓冲区。
//--- 属性 #property indicator_chart_window #property indicator_buffers 11 #property indicator_plots 7 #property indicator_color1 clrMediumSeaGreen #property indicator_color2 clrRed #property indicator_color5 clrMediumSeaGreen #property indicator_color6 clrRed
为了方便,使用的含有类的文件都位于指标目录的 Includes 文件夹下:
图 12. 指标目录.
所以,包含的主文件将看起来如下:
//--- 包含的指标类 #include "Includes\ATR.mqh" #include "Includes\RsiPlus.mqh"
对于外部参数,我们加上了另一个用于 ATR 指标周期数。
//--- 输入参数 input int PeriodRSI =8; // RSI 周期数 input double SignalLevel =30; // 信号水平 input ENUM_BREAK_INOUT BreakMode =BREAK_OUT; // 突破模式 input int PeriodATR =200; // ATR 周期数
这个指标声明的时候会从构造函数传入参数。
//--- 用于工作的指标实例
CATR atr(PeriodATR);
CRsiPlus rsi(PeriodRSI,SignalLevel,BreakMode);
在 OnInit() 初始化函数中,不要忘记把 ATR 的指针传给 RSI 指标。
//+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ void OnInit(void) { //--- 初始化指标 rsi.AtrPointer(atr); //--- 设置指标属性 SetPropertiesIndicator(); }
因为在每个指标类中,为指标缓冲区分配的数组是以公有方式声明的,它们在主文件的指标中看起来就和通常所包含的动态数组一样。
//+------------------------------------------------------------------+ //| 设置指标属性 | //+------------------------------------------------------------------+ void SetPropertiesIndicator(void) { //--- 短名称 ::IndicatorSetString(INDICATOR_SHORTNAME,"RSI_PLUS_CHART"); //--- 小数点位置 ::IndicatorSetInteger(INDICATOR_DIGITS,::Digits()); //--- 指标缓冲区 ::SetIndexBuffer(0,rsi.m_buy_buffer,INDICATOR_DATA); ::SetIndexBuffer(1,rsi.m_sell_buffer,INDICATOR_DATA); ::SetIndexBuffer(2,rsi.m_buy_counter_buffer,INDICATOR_DATA); ::SetIndexBuffer(3,rsi.m_sell_counter_buffer,INDICATOR_DATA); ::SetIndexBuffer(4,rsi.m_buy_level_buffer,INDICATOR_DATA); ::SetIndexBuffer(5,rsi.m_sell_level_buffer,INDICATOR_DATA); ::SetIndexBuffer(6,atr.m_atr_buffer,INDICATOR_DATA); ::SetIndexBuffer(7,rsi.m_rsi_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(8,rsi.m_pos_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(9,rsi.m_neg_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(10,atr.m_tr_buffer,INDICATOR_CALCULATIONS); //--- 初始化数组 atr.ZeroIndicatorBuffers(); rsi.ZeroIndicatorBuffers(); ... }
辅助数组是用于额外计算的,不在图表和数据窗口中显示。需要在数据窗口中显示而不需要在图表上显示的缓冲区,需要给这样的序列设置 DRAW_NONE 属性。
//+------------------------------------------------------------------+ //| 设置指标属性 | //+------------------------------------------------------------------+ void SetPropertiesIndicator(void) { ... //--- 设置指标缓冲区的类型 ENUM_DRAW_TYPE draw_type[]={DRAW_ARROW,DRAW_ARROW,DRAW_NONE,DRAW_NONE,DRAW_LINE,DRAW_LINE,DRAW_NONE}; for(int i=0; i<indicator_plots; i++) ::PlotIndexSetInteger(i,PLOT_DRAW_TYPE,draw_type[i]); ... }
OnCalculate() 函数的内容就是继续调用 ATR 指标和修改过的 RSI 指标的方法。
//+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- 计算 ATR 指标 if(!atr.CalculateIndicatorATR(rates_total,prev_calculated,time,close,high,low)) return(0); //--- 计算 RSI 指标 if(!rsi.CalculateIndicatorRSI(rates_total,prev_calculated,close,spread)) return(0); //--- 返回最近计算的元件数量 return(rates_total); }
在编译并在图表中载入此指标之后,您将会看到图13种展示的结果。根据指标信号形成通道画出的水平线,很容易使用它来确定价格从之前序列的反向信号到最近的距离。
图 13. 修改过的 RSI 指标在主图表上的运行结果。
当开发一个交易系统时,可能需要可视化另一个指标缓冲区的值,这在一些缓冲区脱离视线无法在图表上显示的时候更是如此,它们的数值可以在 数据窗口 中看到。为了方便,您可以使用鼠标中间的按钮来启用它。在图 14 中, 来自标准分发包的 ATR指标被加在图表上,所以可以把它与修改过的 RSI 指标做比较。
图 14. 在数据窗口中查看数据。
如果我们需要一个指标,可以在双向同时显示信号,那会是怎么样的呢?毕竟, MetaTrader 5 可以开启对冲账户,也就是说可以开发一个系统来操作不同方向上的仓位。如果有指标可以在有趋势和盘整的时候都生成信号,那将非常有用。
让我们简要讨论一下如何创建这样的系统。我们已经有了必要的一切,修改将只会在主文件中进行。这个版本将一共有 20 个缓冲区, 其中有 15 个将用于画图。
#property indicator_buffers 20 #property indicator_plots 15
基于趋势的信号将以箭头方式显示,对于基于盘整的信号使用的是点形。这将能够更好地显示特定信号属于哪一类,使它更容易理解。
//--- 用于信号的标志: 159 - 点; 233/234 - 箭头; #define ARROW_BUY_IN 233 #define ARROW_SELL_IN 234 #define ARROW_BUY_OUT 159 #define ARROW_SELL_OUT 159
和前一版本一样,包含的是同样的文件,也都位于指标的本地目录中,这样,您就可以在想要的地方放置这些文件的副本。
//--- 包含的指标类 #include "Includes\ATR.mqh" #include "Includes\RsiPlus.mqh"
在指标的这个版本中,不需要在外部参数中指定信号的类型,但是,让我们可以独立指定用于基于趋势信号的水平 (Signal level In) 和基于盘整信号的水平 (Signal level Out) 。
//--- 输入参数 input int PeriodRSI =8; // RSI 周期数 input double SignalLevelIn =35; // Signal level In input double SignalLevelOut =30; // Signal level Out input int PeriodATR =100; // ATR 周期数
那么有必要声明两个 CRsiPlus 类的实例: 一个用于根据趋势的信号, 而另一个用于盘整时的信号,在这两种情况中,都是使用了反向类型的信号 (BREAK_IN_REVERSE 和 BREAK_OUT_REVERSE),也就是说,波动超出通道将会作为盘整时期的信号,而对于趋势时期,入场将在趋势方向上有回滚的时候入场。
//--- 用于工作的指标实例 CATR atr(PeriodATR); CRsiPlus rsi_in(PeriodRSI,SignalLevelIn,BREAK_IN_REVERSE); CRsiPlus rsi_out(PeriodRSI,SignalLevelOut,BREAK_OUT_REVERSE);
ATR 指标的指针应当被传给这两个 RSI 实例:
//+------------------------------------------------------------------+ //| 自定义指标初始化函数 | //+------------------------------------------------------------------+ void OnInit(void) { //--- 传入 ATR 的指针 rsi_in.AtrPointer(atr); rsi_out.AtrPointer(atr); //--- 设置指标属性 SetPropertiesIndicator(); }
每个实例的指标缓冲区都是以这样的顺序设置,在浏览代码时会更加简单,具体顺序当然没有关系,您只需要知道晚点再 EA 交易中根据缓冲区编号来从特定的序列中生成条件就好。
//+------------------------------------------------------------------+ //| 设置指标属性 | //+------------------------------------------------------------------+ void SetPropertiesIndicator(void) { //--- 短名称 ::IndicatorSetString(INDICATOR_SHORTNAME,"RSI_PLUS2_CHART"); //--- 小数点位置 ::IndicatorSetInteger(INDICATOR_DIGITS,::Digits()); //--- 指标缓冲区 ::SetIndexBuffer(0,rsi_in.m_buy_buffer,INDICATOR_DATA); ::SetIndexBuffer(1,rsi_in.m_sell_buffer,INDICATOR_DATA); ::SetIndexBuffer(2,rsi_in.m_buy_counter_buffer,INDICATOR_DATA); ::SetIndexBuffer(3,rsi_in.m_sell_counter_buffer,INDICATOR_DATA); ::SetIndexBuffer(4,rsi_in.m_buy_level_buffer,INDICATOR_DATA); ::SetIndexBuffer(5,rsi_in.m_sell_level_buffer,INDICATOR_DATA); ::SetIndexBuffer(6,rsi_in.m_rsi_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(7,rsi_in.m_pos_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(8,rsi_in.m_neg_buffer,INDICATOR_CALCULATIONS); //--- ::SetIndexBuffer(9,rsi_out.m_buy_buffer,INDICATOR_DATA); ::SetIndexBuffer(10,rsi_out.m_sell_buffer,INDICATOR_DATA); ::SetIndexBuffer(11,rsi_out.m_buy_counter_buffer,INDICATOR_DATA); ::SetIndexBuffer(12,rsi_out.m_sell_counter_buffer,INDICATOR_DATA); ::SetIndexBuffer(13,rsi_out.m_buy_level_buffer,INDICATOR_DATA); ::SetIndexBuffer(14,rsi_out.m_sell_level_buffer,INDICATOR_DATA); ::SetIndexBuffer(15,rsi_out.m_rsi_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(16,rsi_out.m_pos_buffer,INDICATOR_CALCULATIONS); ::SetIndexBuffer(17,rsi_out.m_neg_buffer,INDICATOR_CALCULATIONS); //--- ::SetIndexBuffer(18,atr.m_atr_buffer,INDICATOR_DATA); ::SetIndexBuffer(19,atr.m_tr_buffer,INDICATOR_CALCULATIONS); //--- 初始化数组 atr.ZeroIndicatorBuffers(); rsi_in.ZeroIndicatorBuffers(); rsi_out.ZeroIndicatorBuffers(); ... }
为了防止哪个信号属于哪一类会混淆,对于盘整的信号将会用虚线画出。
//+------------------------------------------------------------------+ //| 设置指标属性 | //+------------------------------------------------------------------+ void SetPropertiesIndicator(void) { ... //--- 为指定的指标缓冲区设置线形 ::PlotIndexSetInteger(13,PLOT_LINE_STYLE,STYLE_DOT); ::PlotIndexSetInteger(14,PLOT_LINE_STYLE,STYLE_DOT); ... }
OnCalculate() 函数的代码只是调用每个指标实例的方法。
//+------------------------------------------------------------------+ //| 自定义指标迭代函数 | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- 计算 ATR 指标 if(!atr.CalculateIndicatorATR(rates_total,prev_calculated,time,close,high,low)) return(0); //--- 计算 RSI 指标 if(!rsi_in.CalculateIndicatorRSI(rates_total,prev_calculated,close,spread)) return(0); if(!rsi_out.CalculateIndicatorRSI(rates_total,prev_calculated,close,spread)) return(0); //--- 返回最近计算的元件数量 return(rates_total); }
图 15 演示了图表中指标的运行,所有这些只是运行 RSI 的结果。
图 15. 通用的 RSI 指标The universal RSI indicator.
图 16. 在策略测试器中的通用 RSI 指标
以此方式,指标显示了入场点以及加仓点,另外,也可以取得当前信号在它所属于的连续信号序列中的索引。因为还有价格水平,也可以获得价格距离之前序列中最后一个信号所移动的距离。也可以实现一种更加复杂的交易算法,例如,当根据趋势开启仓位时,它们可以根据工作于盘整时期的信号来进行部分或者全部关闭。
通过研究图表上指标的结果,您可以得出不同的交易算法,首先需要在测试器中做检查。这里的工作量也很大: 可能会有很多实现的变数,每个交易模块都可能得到其他模块的交易结果,也就是说,趋势交易模块可能会受到盘整交易模块的交易结果,反之亦然。根据这些数据,它们可以调整行为以适应当前的状况。每个模块可以相互影响交易: 修改开启、关闭或者管理仓位的条件,修改管理系统(把它从一种保守模式转换为激进模式或者相反),交易系统可能会非常复杂并与当前的价格行为相适应。
您可以看到,一个简单的指标可以变得提供更多信息,然后,这可以应用于交易系统的开发。否则,所有这些都必须在 EA 交易中完成,使它的代码更加复杂。现在一切都隐藏在一个程序中,而您可以简单地在它的数组中读取计算得到的数值,
您可以按照这个思路继续开发。例如,您可以为新功能引入额外的缓冲区指示有趋势或者盘整的状态,您可以加上一个缓冲区用于跟踪止损,其计算可以绑定于当前波动性 (ATR 指标)。这样,所有所需计算都将在指标内实现,而 EA 交易将只需要取得准备好的水平。
文件名 | 注释 |
---|---|
MQL5\Indicators\RSI\RSI_Plus1.mq5 | 第一版修改过的 RSI 指标 |
MQL5\Indicators\RSI\RSI_Plus2.mq5 | 第二版修改过的 RSI 指标 |
MQL5\Indicators\RSI\RSI_Plus3.mq5 | 第三版修改过的 RSI 指标 |
MQL5\Indicators\RSI\ChartRSI_Plus1\ChartRSI_Plus1.mq5 | 第四版修改过的 RSI 指标 |
MQL5\Indicators\RSI\ChartRSI_Plus2\ChartRSI_Plus2.mq5 | 第五版修改过的 RSI 指标 |
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程