当开发交易算法时,我们几乎总是会遇到一个难题:如何确定趋势/盘整在哪里开始和结束? 很难找到一个完美的解决方案。这个目标看起来可以通过把基于趋势和基于盘整的策略组合为一个算法来实现。在本文中,我们将会创建一个通用的指标,它将组合来自不同类型策略的信号。我们将会尝试在 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 指标的信号
修改 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
指标线与边界的交叉也可以既作为买入也作为卖出信号,所以,外部参数将需要一个枚举,它可以用来指定如何解释指标信号。
- Break in — 突破进入边界的范围。向上突破底部边界是一个买入信号,而向下突破顶部边界是一个卖出信号。
- Break in reverse — 突破进入边界的范围 (与波动方向相反). 与 'Break in' 模式相同的信号,只是买入和卖出的条件相反。
- Break out — 突破出边界的范围。向上突破顶部边界是买入信号,而向下突破底部边界是卖出信号。
- Break out reverse — 突破出边界的范围 (与波动方向相反). 与 'Break out' 模式相同的信号, 只是买入和卖出的条件相反.
所有这些模式将显示在下面的图表中。
//--- 用于突破通道边界模式的枚举 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 Period — 指标周期数;
- Signal Level — 指标水平线;
- Break Mode — 水平突破模式.
//--- 输入参数 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 指标之后,它所生成的错误信号的数量看起来好转了。分析了图形之后,我们可以下结论,这个指标有的时候在盘整的时候效果更好,有的时候在有趋势的时候更好。
- Break in 模式是设计用于在盘整时候在 RSI 的方向上进行交易。
- Break in reverse 模式是设计用于在跟随趋势交易时与 RSI 相反的方向上进行交易。
- Break out 模式是设计用于在跟随趋势交易时与 RSI 相同的方向上进行交易。
- Break out reverse 模式是设计用于在盘整时在与 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[]);