MQL5 酷客宝典 - 轴点交易信号

Denis Kirichenko | 22 五月, 2017


简介

这篇文章继续阐述指标和生成交易信号的设置。这一次,我们将看看轴点 - 反转水平(点),我们会再次使用标准库(Standard Library)。首先,我们将讨论反转水平指标,根据它开发一个基本策略,并最终找到方法来对它进行加强。

假定读者对用于开发交易信号生成器的CExpertSignal基类已经熟悉了。


1. 轴点(反转水平)指标

对于这个策略,我们将使用指标来绘制潜在的反转水平,绘图是通过图形方式构建的,而没有使用图形对象。这种方法的主要优点是可以在优化模式下参考指标,另一方面,图形化构建不能超出指标的缓冲区,也就意味着没有出现在将来的线。

有几种方法可以计算水平,关于这个主题的更多信息可以在文章 "基于轴点分析的交易策略" 中找到。

让我们现在考虑一下标准方法 (使用以下公式定义水平线):



RES 是一个阻力水平,而 SUP 是支撑水平。一共有1个主反转水平 (PP), 6个阻力水平 (RES) 和6个支撑水平(SUP)。

所以,从视觉上指标看起来就像一系列在不同价格上画出的水平线。当在图表上第一次载入时,指标只会在当前日内绘制水平线 (图1).


图1. 轴点指标: 绘制当前日

图1. 轴点指标: 绘制当前日


让我们分块查看指标代码,从第一个开始计算,

当新的一天开始时,我们需要计算所有的反转水平。

//--- 如果是新的一天
   if(gNewDay.isNewBar(today))
     {
      PrintFormat("新的一天: %s",TimeToString(today));
      //--- 规范化价格
      double d_high=NormalizeDouble(daily_rates[0].high,_Digits);
      double d_low=NormalizeDouble(daily_rates[0].low,_Digits);
      double d_close=NormalizeDouble(daily_rates[0].close,_Digits);
      //--- 保存价格
      gYesterdayHigh=d_high;
      gYesterdayLow=d_low;
      gYesterdayClose=d_close;
      //--- 1) pivot: PP = (HIGH + LOW + CLOSE) / 3        
      gPivotVal=NormalizeDouble((gYesterdayHigh+gYesterdayLow+gYesterdayClose)/3.,_Digits);
      //--- 4) RES1.0 = 2*PP - LOW
      gResVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayLow,_Digits);
      //--- 5) SUP1.0 = 2*PP – HIGH
      gSupVal_1_0=NormalizeDouble(2.*gPivotVal-gYesterdayHigh,_Digits);
      //--- 8) RES2.0 = PP + (HIGH -LOW)
      gResVal_2_0=NormalizeDouble(gPivotVal+(gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 9) SUP2.0 = PP - (HIGH – LOW)
      gSupVal_2_0=NormalizeDouble(gPivotVal-(gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 12) RES3.0 = 2*PP + (HIGH – 2*LOW)
      gResVal_3_0=NormalizeDouble(2.*gPivotVal+(gYesterdayHigh-2.*gYesterdayLow),_Digits);
      //--- 13) SUP3.0 = 2*PP - (2*HIGH – LOW)
      gSupVal_3_0=NormalizeDouble(2.*gPivotVal-(2.*gYesterdayHigh-gYesterdayLow),_Digits);
      //--- 2) RES0.5 = (PP + RES1.0) / 2
      gResVal_0_5=NormalizeDouble((gPivotVal+gResVal_1_0)/2.,_Digits);
      //--- 3) SUP0.5 = (PP + SUP1.0) / 2
      gSupVal_0_5=NormalizeDouble((gPivotVal+gSupVal_1_0)/2.,_Digits);
      //--- 6) RES1.5 = (RES1.0 + RES2.0) / 2
      gResVal_1_5=NormalizeDouble((gResVal_1_0+gResVal_2_0)/2.,_Digits);
      //--- 7) SUP1.5 = (SUP1.0 + SUP2.0) / 2
      gSupVal_1_5=NormalizeDouble((gSupVal_1_0+gSupVal_2_0)/2.,_Digits);
      //--- 10) RES2.5 = (RES2.0 + RES3.0) / 2
      gResVal_2_5=NormalizeDouble((gResVal_2_0+gResVal_3_0)/2.,_Digits);
      //--- 11) SUP2.5 = (SUP2.0 + SUP3.0) / 2
      gSupVal_2_5=NormalizeDouble((gSupVal_2_0+gSupVal_3_0)/2.,_Digits);
      //--- 当前日的开始柱
      gDayStart=today;
      //--- 寻找活动时段的开始柱
      //--- 形式为时间序列
      for(int bar=0;bar<rates_total;bar++)
        {
         //--- 选中的柱的时间
         datetime curr_bar_time=time[bar];
         user_date.DateTime(curr_bar_time);
         //--- 选中的柱的日期
         datetime curr_bar_time_of_day=user_date.DateOfDay();
         //--- 如果当前柱是前一天的柱
         if(curr_bar_time_of_day<gDayStart)
           {
            //--- 保存起始柱
            gBarStart=bar-1;
            break;
           }
        }
      //--- 重置本地计数器
      prev_calc=0;
             }

使用红色 突出显示的字符串是重新计算的水平线。下面,我们应该找到当前时段的柱作为绘制水平的起始点,它的数值是在 gBarStart 变量中定义的。SUserDateTime 自定义结构 (派生于CDateTime结构)是在操作日期和时间时用于搜索的,

现在,让我们集中精力于填充当前时段柱的缓冲区数值模块。

//--- 活动时段中是否有新柱
   if(gNewMinute.isNewBar(time[0]))
     {
      //--- 进行计算的柱的编号 
      int bar_limit=gBarStart;
      //--- 如果这不是第一次载入
      if(prev_calc>0)
         bar_limit=rates_total-prev_calc;
      //--- 计算缓冲区 
      for(int bar=0;bar<=bar_limit;bar++)
        {
         //--- 1) 轴点
         gBuffers[0].data[bar]=gPivotVal;
         //--- 2) RES0.5
         if(gToPlotBuffer[1])
            gBuffers[1].data[bar]=gResVal_0_5;
         //--- 3) SUP0.5
         if(gToPlotBuffer[2])
            gBuffers[2].data[bar]=gSupVal_0_5;
         //--- 4) RES1.0
         if(gToPlotBuffer[3])
            gBuffers[3].data[bar]=gResVal_1_0;
         //--- 5) SUP1.0
         if(gToPlotBuffer[4])
            gBuffers[4].data[bar]=gSupVal_1_0;
         //--- 6) RES1.5
         if(gToPlotBuffer[5])
            gBuffers[5].data[bar]=gResVal_1_5;
         //--- 7) SUP1.5
         if(gToPlotBuffer[6])
            gBuffers[6].data[bar]=gSupVal_1_5;
         //--- 8) RES2.0
         if(gToPlotBuffer[7])
            gBuffers[7].data[bar]=gResVal_2_0;
         //--- 9) SUP2.0
         if(gToPlotBuffer[8])
            gBuffers[8].data[bar]=gSupVal_2_0;
         //--- 10) RES2.5
         if(gToPlotBuffer[9])
            gBuffers[9].data[bar]=gResVal_2_5;
         //--- 11) SUP2.5
         if(gToPlotBuffer[10])
            gBuffers[10].data[bar]=gSupVal_2_5;
         //--- 12) RES3.0
         if(gToPlotBuffer[11])
            gBuffers[11].data[bar]=gResVal_3_0;
         //--- 13) SUP3.0
         if(gToPlotBuffer[12])
            gBuffers[12].data[bar]=gSupVal_3_0;
        }
             }

当新柱在载入指标的图表上出现时开始缓冲区的计算,黄色突出显示了直到需要计算的缓冲区的柱的编号,已计算的柱数的本地计数器就是用于它的,我们需要它是因为在新一天的开始时把 prev_calculated 常量重设为0, 尽管这样的重置是有必要的。

轴点指标的完整代码可以在 Pivots.mq5 文件中找到。


2. 基本策略

让我们根据所描述的指标来开发一个简单的基本策略,让我们根据开盘价格相对中心轴点的位置来作为建仓信号,价格触碰到轴点水平作为确认信号。

EURUSD M15 图表 (图2) 显示了低于中心轴点的开盘价格的一天 (2015年1月15日)。但是,在这一天的晚些时候,价格上升达到了轴点水平,这样,就有了一个卖出信号。如果没有激活止损或者获利,就在第二天的开始退出市场。

图2基本策略:卖出信号

图2基本策略:卖出信号


止损水平是和轴点指标反转水平绑定的,中间的阻力水平 Res0.5,价格 $1.18153 在卖出时作为止损,主支撑水平 Sup1.0,价格 $1.17301 用于作为获利水平。我们将在晚些时候回到1月14日的交易日,同时,让我们看一下实现基本策略部分的代码。


2.1 CSignalPivots 信号类

让我们创建一个信号类来根据价格动态和反转水平指标所形成的各种模式来生成信号。

//+------------------------------------------------------------------+
//| Class CSignalPivots                                              |
//| 目的: 根据轴点生成交易信号的类.                                       |
//| CExpertSignal 类的继承类.                                          |
//+------------------------------------------------------------------+
class CSignalPivots : public CExpertSignal
  {
   //--- === 数据成员 === --- 
protected:
   CiCustom          m_pivots;            // "Pivots(轴点)" 指标对象   
   //--- adjustable parameters
   bool              m_to_plot_minor;     // 画出次级水平
   double            m_pnt_near;          // 阈值 
   //--- estimated
   double            m_pivot_val;         // 轴点数值
   double            m_daily_open_pr;     // 当前日的开盘价   
   CisNewBar         m_day_new_bar;       // 每日时段的新柱
   //--- 市场模式  
   //--- 1) 模式 0 "第一次接触到轴点水平" (顶部 - 买入, 底部 - 卖出)
   int               m_pattern_0;         // 权重
   bool              m_pattern_0_done;    // 模式结束的标记
   //--- === 方法 === --- 
public:
   //--- 构造函数/析构函数
   void              CSignalPivots(void);
   void             ~CSignalPivots(void){};
   //--- 设置可以调节的参数的方法
   void              ToPlotMinor(const bool _to_plot) {m_to_plot_minor=_to_plot;}
   void              PointsNear(const uint _near_pips);
   //--- 调整市场模式 "权重" 的方法
   void              Pattern_0(int _val) {m_pattern_0=_val;m_pattern_0_done=false;}
   //--- 确认设置的方法
   virtual bool      ValidationSettings(void);
   //--- 创建指标和时间序列的方法
   virtual bool      InitIndicators(CIndicators *indicators);
   //--- 检查是否生成了市场模式的方法
   virtual int       LongCondition(void);
   virtual int       ShortCondition(void);
   virtual double    Direction(void);
   //--- 用于侦测进入市场水平的方法
   virtual bool      OpenLongParams(double &price,double &sl,double &tp,datetime &expiration);
   virtual bool      OpenShortParams(double &price,double &sl,double &tp,datetime &expiration);
   //---
protected:
   //--- 用于初始化指标的方法
   bool              InitCustomIndicator(CIndicators *indicators);
   //--- 取得轴点水平值
   double            Pivot(void) {return(m_pivots.GetData(0,0));}
   //--- 取得主阻力水平的数值
   double            MajorResistance(uint _ind);
   //--- 取得所需的次级阻力水平数值
   double            MinorResistance(uint _ind);
   //--- 取得主支撑水平数值
   double            MajorSupport(uint _ind);
   //--- 取得次级支撑水平数值
   double            MinorSupport(uint _ind);
  };
    //+------------------------------------------------------------------+


我已经使用了这篇文章 "MQL5 酷客宝典 - 移动通道的交易信号"中的方法: 当价格下跌到线内区域时,价格触线就被确认。m_pnt_near 数据成员设置了反转水平的阈值。

类中提供的信号模式是最重要的角色,基类有一个单独的模式,除了来自权重 (m_pattern_0), 它还含有一个交易日内的结束属性 (m_pattern_0_done).

CExpertSignal 基信号类中有很多虚拟方法,这使得可以在派生类中实现更好的调整,

特别的一点是,我已经重新定义了 OpenLongParams()OpenShortParams() 方法用来计算交易水平,

让我们查看第一个方法的代码 — 定义当买入时候的交易水平数值。

//+------------------------------------------------------------------+
//| 定义买入时的交易水平数值                                              |
//+------------------------------------------------------------------+
bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   bool params_set=false;
   sl=tp=WRONG_VALUE;
//--- 如果使用了模式 0 
   if(IS_PATTERN_USAGE(0))
      //--- 如果模式 0 没有完成
      if(!m_pattern_0_done)
        {
         //--- 开盘价 - 市场
         double base_price=m_symbol.Ask();
         price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
         //--- 止损价 - Sup0.5 水平
         sl=this.MinorSupport(0);
         if(sl==DBL_MAX)
            return false;
         //--- 如果设置了止损价格
         sl=m_symbol.NormalizePrice(sl);
         //--- 获利价格 - Res1.0 水平         
         tp=this.MajorResistance(0);
         if(tp==DBL_MAX)
            return false;
         //--- 如果设置了获利价格
         tp=m_symbol.NormalizePrice(tp);
         expiration+=m_expiration*PeriodSeconds(m_period);
         //--- 如果设置了价格
         params_set=true;
         //--- 模式结束
         m_pattern_0_done=true;
        }
//---
   return params_set;
  }
    //+------------------------------------------------------------------+


止损价格是使用 MinorSupport() 方法计算得到的第一个二级支撑水平的数值,获利是使用了 MajorResistance() 方法计算得到的第一个主阻力水平。如果是卖出,方法会分别使用 MinorResistance() MajorSupport() 来对应地替换。

把自定义信号变成主信号,以使得用于定义交易水平的方法可以正常工作,这里就是父类中的方法是如何定义交易水平的,看起来是这样的:

//+------------------------------------------------------------------+
//| 侦测用于买入的水平                                                  |
//+------------------------------------------------------------------+
bool CExpertSignal::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   CExpertSignal *general=(m_general!=-1) ? m_filters.At(m_general) : NULL;
//---
   if(general==NULL)
     {
      //--- 如果没有明确指定基础价格,就是用当前市场价格
      double base_price=(m_base_price==0.0) ? m_symbol.Ask() : m_base_price;
      price      =m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
      sl         =(m_stop_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price-m_stop_level*PriceLevelUnit());
      tp         =(m_take_level==0.0) ? 0.0 : m_symbol.NormalizePrice(price+m_take_level*PriceLevelUnit());
      expiration+=m_expiration*PeriodSeconds(m_period);
      return(true);
     }
//---
   return(general.OpenLongParams(price,sl,tp,expiration));
  }
    //+------------------------------------------------------------------+

如果没有设置主信号索引,水平就使用默认值,为了避免这样, 在EA代码中初始化信号时,做以下设置:

//--- CSignalPivots 过滤器
   CSignalPivots *filter0=new CSignalPivots;
   if(filter0==NULL)
     {
      //--- 错误
      PrintFormat(__FUNCTION__+": 创建 filter0 出错");
      return INIT_FAILED;
     }
   signal.AddFilter(filter0);
       signal.General(0);  


买入条件的验证方法如下所示:

//+------------------------------------------------------------------+
//| 检查买入条件                                                       |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- 如果没有使用模式 0 
   if(IS_PATTERN_USAGE(0))
      //--- 如果模式 0 没有完成
      if(!m_pattern_0_done)
         //--- 如果一天的开盘价高于轴点
         if(m_daily_open_pr>m_pivot_val)
           {
            //--- 当前柱的最低价格
            double last_low=m_low.GetData(1);
            //--- 如果得到了价格
            if((last_low>WRONG_VALUE) && (last_low<DBL_MAX))
               //--- 如果有上方的接触 (考虑到阈值)
               if(last_low<=(m_pivot_val+m_pnt_near))
                 {
                  result=m_pattern_0;
                  //--- 写到日志中
                  Print("\n---== 价格在上方接触到轴点水平 ==---");
                  PrintFormat("价格: %0."+IntegerToString(m_symbol.Digits())+"f",last_low);
                  PrintFormat("轴点: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val);
                  PrintFormat("阈值: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near);
                 }
           }
//---
   return result;
  }
    //+------------------------------------------------------------------+

很容易可以看到,从上方的接触在考虑阈值时做了检查 last_low<=(m_pivot_val+m_pnt_near).

除了其他的一些,如果基本模式没有完成,Direction() 方法定义了"有权重的"方向检查

//+------------------------------------------------------------------+
//| 定义 "有权重的" 方向                                                |
//+------------------------------------------------------------------+
double CSignalPivots::Direction(void)
  {
   double result=0.;
//--- 接收每日历史数据
   MqlRates daily_rates[];
   if(CopyRates(_Symbol,PERIOD_D1,0,1,daily_rates)<0)
      return 0.;
//--- 如果模式 0 已经完成
   if(m_pattern_0_done)
     {
      //--- 检查新的一天
      if(m_day_new_bar.isNewBar(daily_rates[0].time))
        {
         //--- 重设模式完成标记
         m_pattern_0_done=false;
         return 0.;
        }
     }
//--- 如果模式 0 没有完成
   else
     {
      //--- 每日开盘价
      if(m_daily_open_pr!=daily_rates[0].open)
         m_daily_open_pr=daily_rates[0].open;
      //--- 轴点
      double curr_pivot_val=this.Pivot();
      if(curr_pivot_val<DBL_MAX)
         if(m_pivot_val!=curr_pivot_val)
            m_pivot_val=curr_pivot_val;
     }
//--- 结果
   result=m_weight*(this.LongCondition()-this.ShortCondition());
//---
   return result;
  }
    //+------------------------------------------------------------------+


对于退出信号,重新定义父类方法 CloseLongParams()CloseShortParams(). 买入代码模块例子:

//+------------------------------------------------------------------+
//| 定义买入时的交易水平                                                 |
//+------------------------------------------------------------------+
bool CSignalPivots::CloseLongParams(double &price)
  {
   price=0.;
//--- 如果使用了模式 0 
   if(IS_PATTERN_USAGE(0))
      //--- 如果模式 0 没有完成
      if(!m_pattern_0_done)
        {
         price=m_symbol.Bid();
         //--- 写到日志中
         Print("\n---== 关闭买入仓位的信号 ==---");
         PrintFormat("市场价格: %0."+IntegerToString(m_symbol.Digits())+"f",price);
         return true;
        }
//--- 返回结果
   return false;
  }
    //+------------------------------------------------------------------+

退出信号的阈值在EA交易代码中应当重置为0。

signal.ThresholdClose(0);

在这种情况下没有进行 方向的检查

//+------------------------------------------------------------------+
//| 生成关闭买入仓位的信号                                               |
//+------------------------------------------------------------------+
bool CExpertSignal::CheckCloseLong(double &price)
  {
   bool   result   =false;
//--- "禁止"信号
   if(m_direction==EMPTY_VALUE)
      return(false);
//--- 检查是否超过阈值
   if(-m_direction>=m_threshold_close)
     {
      //--- 有信号
      result=true;
      //--- 尝试取得平仓水平
      if(!CloseLongParams(price))
         result=false;
     }
//--- 基础价格清零
   m_base_price=0.0;
//--- 返回结果
   return(result);
  }
    //+------------------------------------------------------------------+

产生了这样的问题: 在这种情况下怎样检查退出信号呢?首先,检查仓位是否存在 (在 Processing() 方法中 ), 然后,使用 m_pattern_0_done 属性 (在 CloseLongParams()CloseShortParams() 方法中重新定义). 一旦EA侦测到有仓位,并且模式0没有完成,它就会尝试立即关闭它,这在交易日的开始会发生。

我们已经检视了 CSignalPivots 信号类的基础,现在,让我们探讨策略类。


2.2 CPivotsExpert 交易策略类

派生的策略类和移动通道的很类似,第一个不同点是使用了按分钟交易的模式而不是按分时交易的模式。这使你可以在相对较深的历史中快速测试策略。第二,还有对退出的检查。我们已经在EA可能平仓的时候定义了它。

主要的处理方法看起来如下:

//+------------------------------------------------------------------+
//| 主模块                                                            |
//+------------------------------------------------------------------+
bool CPivotsExpert::Processing(void)
  {
//--- 新的分钟柱
   if(!m_minute_new_bar.isNewBar())
      return false;
//--- 计算方向
   m_signal.SetDirection();
//--- 如果没有仓位
   if(!this.SelectPosition())
     {
      //--- 仓位开启模块
      if(this.CheckOpen())
         return true;
     }
//--- 如果有仓位 
   else
     {
      //--- 仓位关闭模块
      if(this.CheckClose())
         return true;
     }
//--- 如果没有交易操作
   return false;
  }
    //+------------------------------------------------------------------+

就是这样。现在,我们可以运行基本策略了,它的代码在 BasePivotsTrader.mq5 文件中。


图3. 基本策略: 卖出

图3. 基本策略: 卖出


让我们回到2015年1月14日这一天,在这种条件下,模型工作得很好。我们在轴点建立卖出仓位并且在主支撑水平 Sup1.0 平仓。

在策略测试器中从2013年1月7日到2017年1月7日,在 EURUSD M15 中使用以下参数运行:

  • Entry signal threshold(进场信号阈值), [0...100] = 10;
  • Weight(权重), [0...1.0] = 1,0;
  • Fixed volume (固定交易量) = 0,1;
  • Tolerance, points(阈值,点数) = 15.

可以发现,策略交易的结果很稳定。是负面的 (图 4).

Fig.4. EURUSD: 2013-2016 第一个基本策略的结果

图4. EURUSD: 2013-2016 第一个基本策略的结果


根据结果判断,我们做错了每一件事,我们应当在卖出信号时买入而在买入信号时卖出,但这是真的吗?让我们看看。如果要这样做,我们应当开发一个基本策略并且在信号中做一些改变,在这种情况下,买入条件应当看起来如下:

//+------------------------------------------------------------------+
//| 检查卖出条件                                                       |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- 如果没有使用模式 0 
   if(IS_PATTERN_USAGE(0))
      //--- 如果模式 0 没有完成
      if(!m_pattern_0_done)
         //--- 如果一天的开盘价低于轴点
         if(m_daily_open_pr<m_pivot_val)
           {
            //--- 当前柱的最高价
            double last_high=m_high.GetData(1);
            //--- 如果得到了价格
            if((last_high>WRONG_VALUE) && (last_high<DBL_MAX))
               //--- 如果有上方的接触 (考虑到阈值)
               if(last_high>=(m_pivot_val-m_pnt_near))
                 {
                  result=m_pattern_0;
                  //--- 写到日志中
                  Print("\n---== 价格从下方接触到轴点水平 ==---");
                  PrintFormat("Price: %0."+IntegerToString(m_symbol.Digits())+"f",last_high);
                  PrintFormat("轴点: %0."+IntegerToString(m_symbol.Digits())+"f",m_pivot_val);
                  PrintFormat("阈值: %0."+IntegerToString(m_symbol.Digits())+"f",m_pnt_near);
                 }
           }
//---
   return result;
  }
    //+------------------------------------------------------------------+

让我们在策略测试器中运行另一个策略并取得结果:

图5. EURUSD: 2013-2016 第二个基本策略的运行结果

图5. EURUSD: 2013-2016 第二个基本策略的运行结果

显然,并没有出现第一个版本的镜像,也许,原因是止损和获利值。另外,如果在交易日中没有触发止损水平,新的一天开始时会关闭仓位。

让我们尝试改变第二个版本的基本策略, 这样在买入的恶时候止损水平就更远了 — 在主支撑水平 Sup1.0 之前, 儿利润大小被限制在中间的阻力水平 Res0.5. 当卖出时,止损就放置在 Res1.0, 而获利位于 — Sup0.5.

在这种情况下, 买入交易水平是这样定义的:

//+------------------------------------------------------------------+
//| 定义买入交易的水平                                                   |
//+------------------------------------------------------------------+
bool CSignalPivots::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   bool params_set=false;
   sl=tp=WRONG_VALUE;
//--- 如果使用了模式 0 
   if(IS_PATTERN_USAGE(0))
      //--- 如果模式 0 没有完成
      if(!m_pattern_0_done)
        {
         //--- 开盘价 - 市场
         double base_price=m_symbol.Ask();
         price=m_symbol.NormalizePrice(base_price-m_price_level*PriceLevelUnit());
         //--- 止损价格e - Sup1.0 水平
         sl=this.MajorSupport(0);
         if(sl==DBL_MAX)
            return false;
         //--- 如果设置了止损价格
         sl=m_symbol.NormalizePrice(sl);
         //--- 获利价格 - Res0.5 水平         
         tp=this.MinorResistance(0);
         if(tp==DBL_MAX)
            return false;
         //--- 如果设置了获利价格
         tp=m_symbol.NormalizePrice(tp);
         expiration+=m_expiration*PeriodSeconds(m_period);
         //--- 如果设置了价格
         params_set=true;
         //--- 模式结束
         m_pattern_0_done=true;
        }
//---
   return params_set;
  }
    //+------------------------------------------------------------------+


第三个版本的策略在测试器中的结果如下:

图6. EURUSD: 2013-2016 第三个基本策略的结果

图6. EURUSD: 2013-2016 第三个基本策略的结果


图片和第一个版本的镜像多少有些类似,乍一看来,似乎找到了绝招,但是我们将要讨论一些存在的缺陷。


3. 健壮性

如果我们仔细看了图6,我们可以很容易看到余额曲线增长得不均衡,有些片段中,余额积累的利润稳定增加,也有一些回撤的片段以及余额曲线直接向右移动的情况。

健壮性是一个交易系统的稳定性,指示了它在场时间段内的成绩和效率,

总体看来,我们可以说这个策略缺乏健壮性。有没有可能提高它呢?让我们试试看。


3.1趋势指标

以我的观点,上面描述的交易规则在市场有方向 - 有趋势时工作得更好,策略显示,最佳结果是2014年到2015年早期的 EURUSD,当时该货币对是稳定下跌的。

这就说明我们需要一个过滤器来使我们避免在平盘时交易。有很多资料是关于确定一个稳定趋势的,您可以在 mql5.com 的文章部分找到它们。个人角度,我最喜欢"在MQL5中几种找到趋势的方法"这篇文章。它提供了方便,并且更重要的,通用的方法来寻找趋势。

我已经开发了类似的指标 MaTrendCatcher。它比较快速和慢速移动平均,如果它们之间的差是正的,趋势就是上涨趋势,指标柱形图的柱就等于 1. 如果差是负数,趋势就是下跌的,柱就等于-1 (图 7).


图 7. MaTrendCatcher 趋势指标

Fig.7. MaTrendCatcher 趋势指标


另外,如果移动平均之间的差距相对前一个柱加大了 (趋势变得更强), 柱就是绿色的,否则它是红色的。

在指标中加入了另一项功能: 如果移动品均之间的差很小,就不显示柱。隐藏柱的差距大小的数值,是根据指标参数中的 "Cutoff, pp" (图. 8).


图8. 隐藏了小差距的 MaTrendCatcher 趋势指标

图8. 隐藏了小差距的 MaTrendCatcher 趋势指标


这样,就让我们使用 MaTrendCatcher 指标来进行过滤。

为了使用指标,我们需要在项目文件的代码中做一些修改,请注意,EA的最新版本要保存在 Model 文件夹下

对于这个策略,我们需要取得计算的 "有权重的" 方向的数值,所以,我们需要从基本信号类中派生一个自定义类。

class CExpertUserSignal : public CExpertSignal

然后,在更新的反转水平信号类中出现一个新的模型 — 模型 1 "trend-flat-countertrend".

从本质上,它是模型0的补充。所以,它可以被称为子模式,我们晚些时候会查看代码,

现在,买入条件的确认看起来如下:

//+------------------------------------------------------------------+
//| 检查买入条件                                                       |
//+------------------------------------------------------------------+
int CSignalPivots::LongCondition(void)
  {
   int result=0;
//--- 如果没有使用模式 0 
   if(IS_PATTERN_USAGE(0))
      //--- 如果模式 0 没有完成
      if(!m_pattern_0_done)
        {
         m_is_signal=false;
         //--- 如果一天的开盘价低于轴点
         if(m_daily_open_pr<m_pivot_val)
           {
            //--- 过去柱的最高价
            double last_high=m_high.GetData(1);
            //--- 如果得到了价格
            if(last_high>WRONG_VALUE && last_high<DBL_MAX)
               //--- 如果有上方的接触 (考虑到阈值)
               if(last_high>=(m_pivot_val-m_pnt_near))
                 {
                  result=m_pattern_0;
                  m_is_signal=true;
                  //--- 写到日志中
                  this.Print(last_high,ORDER_TYPE_BUY);
                 }
           }
         //--- 如果使用了模式 1 
         if(IS_PATTERN_USAGE(1))
           {
            //--- 如果之前的柱是上涨趋势
            if(m_trend_val>0. && m_trend_val!=EMPTY_VALUE)
              {
               //--- 如果有加速
               if(m_trend_color==0. && m_trend_color!=EMPTY_VALUE)
                  result+=(m_pattern_1+m_speedup_allowance);
               //--- 如果没有加速
               else
                  result+=(m_pattern_1-m_speedup_allowance);
              }
           }
        }
//---
   return result;
          }

绿色模块突出显示了应用子模式的地方。

计算背后的思路是: 如果不考虑子模式而进场,信号的结果就等于模式0的权重,如果考虑了子模式,就可能有以下选项:

  1. 在有加速趋势时按方向进入市场 (有趋势并且有加速);
  2. 在没有加速趋势时按方向进入市场 (有趋势但没有加速);
  3. 在有加速时反趋势方向进入市场 (反趋势和加速);
  4. 在没有加速时反趋势进入市场 (反趋势而没有加速).

这种方法可以避免回应弱的信号,如果信号权重超出了阈值,它会影响交易量的大小,轴点 EA 类还有 CPivotsExpert::LotCoefficient() 方法的功能:

//+------------------------------------------------------------------+
//| 手数比例                                                           |
//+------------------------------------------------------------------+
double CPivotsExpert::LotCoefficient(void)
  {
   double lot_coeff=1.;
//--- 通用的信号
   CExpertUserSignal *ptr_signal=this.Signal();
   if(CheckPointer(ptr_signal)==POINTER_DYNAMIC)
     {
      double dir_val=ptr_signal.GetDirection();
      lot_coeff=NormalizeDouble(MathAbs(dir_val/100.),2);
     }
//---
   return lot_coeff;
  }
    //+------------------------------------------------------------------+

比如说,如果信号有120级,而初始交易量调整到1.2,而在70的时候,它就要调整到0.7.

为了使用比例,还是需要重新定义 OpenLong() 和 OpenShort() 方法,比如,买入方法就如下所示:

//+------------------------------------------------------------------+
//| 建立买入仓位或者设置限价/止损订单                                      |
//+------------------------------------------------------------------+
bool CPivotsExpert::OpenLong(double price,double sl,double tp)
  {
   if(price==EMPTY_VALUE)
      return(false);
//--- 取得建仓手数
   double lot_coeff=this.LotCoefficient();
   double lot=LotOpenLong(price,sl);
   lot=this.NormalLot(lot_coeff*lot);
//--- 检查建仓手数
   lot=LotCheck(lot,price,ORDER_TYPE_BUY);
   if(lot==0.0)
      return(false);
//---
   return(m_trade.Buy(lot,price,sl,tp));
  }
    //+------------------------------------------------------------------+

动态计算手数大小的想法很简单: 信号越强,风险越大。


3.2范围大小

很容易看到,在市场波动小的时候,反转水平(轴点)相互距离很近,为了避免在这样的日子中进行交易,引入了 "Width limit, pp" 参数。模式 0 (以及子模式) 在没有超出限制的时候要保证考虑到,限制在 Direction() 方法中确认,以下是这部分的代码:

//--- 如果设置了限制
   if(m_wid_limit>0.)
     {
      //--- 评估顶部限制 
      double norm_upper_limit=m_symbol.NormalizePrice(m_wid_limit+m_pivot_val);
      //--- 实际顶部限制
      double res1_val=this.MajorResistance(0);
      if(res1_val>WRONG_VALUE && res1_val<DBL_MAX)
        {
         //--- 如果没有超出限制 
         if(res1_val<norm_upper_limit)
           {
            //--- 模式 0 已经完成
            m_pattern_0_done=true;
            //--- 写到日志中
            Print("\n---== 没有超出顶部限制 ==---");
            PrintFormat("估算: %0."+IntegerToString(m_symbol.Digits())+"f",norm_upper_limit);
            PrintFormat("实际: %0."+IntegerToString(m_symbol.Digits())+"f",res1_val);
            //---
            return 0.;
           }
        }
      //--- 估算的底部限制 
      double norm_lower_limit=m_symbol.NormalizePrice(m_pivot_val-m_wid_limit);
      //--- 真正的底部限制
      double sup1_val=this.MajorSupport(0);
      if(sup1_val>WRONG_VALUE && sup1_val<DBL_MAX)
        {
         //--- 如果没有超出限制 
         if(norm_lower_limit<sup1_val)
           {
            //--- 模式 0 已经完成
            m_pattern_0_done=true;
            //--- 写到日志中
            Print("\n---== 没有超出底部限制 ==---");
            PrintFormat("估计值: %0."+IntegerToString(m_symbol.Digits())+"f",norm_lower_limit);
            PrintFormat("实际值: %0."+IntegerToString(m_symbol.Digits())+"f",sup1_val);
            //---
            return 0.;
           }
        }
             }

如果信号没有通过范围宽度的确认,在日志中会出现下面的内容:

2015.08.19 00:01:00   ---== 没有超出顶部限制 ==---
2015.08.19 00:01:00   评估值: 1.10745
2015.08.19 00:01:00   真实值: 1.10719
  
在这种情况下,缺了26个点的信号就又有效了。


在测试器中使用优化模式运行策略,我使用了下面的优化参数:

  1. "Width limit, pp";
  2. "Tolerance, pp";
  3. "Fast МА";
  4. "Slow МА";
  5. "Cut-off, pp".

最成功的利润看起来是这样的:

Fig.9. EURUSD: 2013-2016 使用了过滤器的策略执行结果

图 9. EURUSD: 2013-2016 使用了过滤器的策略的结果

和期待的一样,有些信号被排除了,余额曲线变得更加平滑,

但是还是有失败。在图表上可以看到,策略生成的余额曲线片段在小范围内波动,在2015年没有明显的利润增加,优化的结果可以在e EURUSD_model.xml 文件中找到.

让我们看一下在其他交易品种中的结果,

图10种显示了 USDJPY 上的最佳运行结果。

图10. USDJPY: 2013-2016 使用过滤器的策略的结果

图10. USDJPY: 2013-2016 使用了过滤器策略的结果

现在,让我们看看闪光点,图11中显示了最好的结果。

图11. XAUUSD: 2013-2016 使用了过滤器的策略的结果

图11. XAUUSD: 2013-2016 使用了过滤器策略的结果。

在这段时间中,贵金属在很窄的范围内交易,所以策略没有给出好的结果,

而对于 GBP, 最佳运行结果显示在图12中。

图12. GBPUSD: 2013-2016 使用过滤器的策略的结果

图.12. GBPUSD: 2013-2016 使用过滤器的策略的结果

GBP 在有趋势方向时交易很好,但是在2015年的回调修正中最终结果被破坏了,

总体来说,该策略在有趋势时工作得最好。

结论

交易策略的开发包含几个阶段,在初始阶段,会构成交易的想法。在大多数情况下,这是一个假定,需要以代码的形式实现它并在测试器中检查。在测试过程中经常需要调整和精化这个假定。这是开发者的标准工作。这里我们使用了相同的方法来开发轴点策略,依我看来, OOP 很大程度简化了任务。

所有优化模式下的测试都是在 MQL5 云网络 中进行的,云技术使我可以使用快速和便宜的方法来有效评估策略的效率。


文件位置

文件位置

最方便的是把策略文件都放到一个 Pivots 文件夹下,把指标文件 (Pivots.ex5 和 MaTrendCatcher.ex5) 编译之后放到 %MQL5\Indicators 指标目录中。