逆转形态:测试双顶/双底形态

25 十二月 2018, 09:02
Dmitriy Gizlyk
0
2 069

内容

概述

在文章“趋势有多长?”中所进行的分析表明价格在 60% 的时间内维持趋势。 这意味着在趋势伊始即开仓会产生最佳结果。 搜索趋势逆转点会产生大量的逆转形态。 双顶/双底是最著名和最常用的形态之一。 

1. 形态形成的理论观点

在价格图表上可以频繁返现双顶/双底形态。 它的形成与交易价位理论密切相关。 当价格达到支撑位或阻力位时(取决于之前的走势),该形态在趋势结束时形成。 在重复测试价位过程中进行调整之后,它会再次回滚而非突破。

在这一点位上,逆势交易者开始从该价位回滚时交易,并将价格推向调整。 在调整走势获得动量的同时,顺势交易者开始获利了结离场,或将亏损持仓平仓来避免突破该价位。 这进一步加强了走势,导致了新趋势的出现。

双顶形态

在图表上搜索形态时,不必搜索顶部/底部的精准匹配。 顶部/底部价位的偏差被认为是正常的。 只需确保峰值在相同的支撑/阻力位之内。 形态可靠性取决于它所基于的级别强度。


2. 形态交易策略

形态的普及引发了多种涉及它的策略。 在互联网上,交易这种形态至少有三个不同的入场点。

2.1. 案例 1

第一个入场点是基于颈线的突破。 止损设置在顶/底线之外。 有不同的方法来定义“颈线突破”。 交易者可以使用在颈线下方收盘的柱线,以及突破颈线一段固定距离的柱线。 两种方法都有其优点和缺点。 在凌厉走势的情况下,蜡烛可以在距颈线足够的距离处收盘,令形态效率很低。

第一个入场点

这种方法的缺点是止损价位相对较高,这降低了策略的利润/风险比。

2.2. 案例 2

第二个入场点基于镜面等级理论,当颈线从支撑变成阻力时,反之亦然。 此处的入场点是在价格突破后回到颈线时进行的。 在这种情况下,止损设置超出最后一次调整的极值,从而显著降低止损价位。 不幸的是,价格并不总是在突破后回测颈线,从而减少了入场次数。

第二个入场点 


2.3. 案例 3

第三个入场点基于趋势理论。 它的定义是突破从走势起点到颈线极值的趋势线。 与第一种情况一样,止损设置在顶/底线之外。 与第一个入场点相比,早期入场提供较低的止损价位。 与第二种情况相比,它还提供更多信号。 与此同时,这样的入场点会发出更多的假信号,因为在极值线和颈部之间可能形成通道,或者可能存在旗形形态。 两种情况都表示趋势延续。

第三个入场点 


所有三种策略都指示在等于极值和颈线之间距离的价位上离场。

止盈

此外,在判断图表上的形态时,您应该注意到双顶/双底应该从价格走势中清晰地脱颖而出。 在描述形态时,通常会添加限制:两个顶部/底部之间应该至少有六根柱线。

此外,由于形态形成是基于价位理论,形态交易不应该与之相矛盾。 因此,基于预期目的,颈线不应低于初始走势的菲波纳奇等级 50。 此外,为了滤除假信号,我们可以添加第一次调整的最低价位(形成颈线)作为价格等级强度的指标。


3. 创建 EA

3.1. 搜索极值

我们将从形态搜索模块开始开发 EA。 我们用 MetaTrader 5 标准发行包中的之字折线指标来搜索价格极值。 将指标计算部分移至文章 [1] 中所述的类。 该指标包含两个指标缓冲区,其中包含极值点的价格值。 指标缓冲区包含极值之间的空值。 为了避免创建两个包含多个空值的指标缓冲区,它们由包含有关极值信息的结构数组所取代。 用于存储有关极值信息的结构如下所示。

   struct s_Extremum
     {
      datetime          TimeStartBar;
      double            Price;
      
      s_Extremum(void)  :  TimeStartBar(0),
                           Price(0)
         {
         }
      void Clear(void)
        {
         TimeStartBar=0;
         Price=0;
        }
     };

如果您至少使用过一次之字折线指标,您就会知道在搜索最佳参数时必须要做出一些妥协。 参数值太小会将大走势分成小部分,而太大的参数值会掩盖短期走势。 搜索形态图形的算法对于寻找极值的品质要求很高。 在尝试寻找中间点的同时,我决定使用具有小参数值的指标,并创建一个额外的上层结构,将单向走势与短期调整合并到一个走势。

CTrends 类是为解决这个问题而开发的。 类的头部提供如下。 在初始化期间,将指标类对象的引用和趋势延续的最小移动值传递给类。

class CTrends : public CObject
  {
private:
   CZigZag          *C_ZigZag;         // 链接到之字折线指标对象
   s_Extremum        Trends[];         // 极值数组
   int               i_total;          // 保存的极值总数
   double            d_MinCorrection;  // 趋势延续的最小移动值

public:
                     CTrends();
                    ~CTrends();
//--- 类初始化方法
   virtual bool      Create(CZigZag *pointer, double min_correction);
//--- 获取有关极值的信息
   virtual bool      IsHigh(s_Extremum &pointer) const;
   virtual bool      Extremum(s_Extremum &pointer, const int position=0);
   virtual int       ExtremumByTime(datetime time);
//--- 获取一般信息
   virtual int       Total(void)          {  Calculate(); return i_total;   }
   virtual string    Symbol(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return "Not Initilized"; return C_ZigZag.Symbol();  }
   virtual ENUM_TIMEFRAMES Timeframe(void) const   {  if(CheckPointer(C_ZigZag)==POINTER_INVALID) return PERIOD_CURRENT; return C_ZigZag.Timeframe();  }
   
protected:
   virtual bool      Calculate(void);
   virtual bool      AddTrendPoint(s_Extremum &pointer);
  };

要获取极值数据,类中提供了以下方法:

  • ExtremumByTime — 获取数据库中指定时间的极值数字,
  • Extremum — 返回数据库中指定位置的极值,
  • IsHigh — 如果指定的极值是顶部,则返回 true; 如果是底部,则返回 false

通用信息模块拥有返回已保存的极值总数,所用品名和时间帧的方法。

类的主要逻辑在 Calculate 方法中实现。 我们来仔细查看。

在方法的开头,检查指标类对象的引用的相关性,以及指标已发现的存在极值。

bool CTrends::Calculate(void)
  {
   if(CheckPointer(C_ZigZag)==POINTER_INVALID)
      return false;
//---
   if(C_ZigZag.Total()==0)
      return true;

接下来,定义未处理的极值数。 如果所有极值已处理,则使用 true 结果退出方法。

   int start=(i_total<=0 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-1].TimeStartBar));
   switch(start)
     {
      case 0:
        return true;
        break;
      case -1:
        start=(i_total<=1 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-2].TimeStartBar));
        if(start<0 || ArrayResize(Trends,i_total-1)<=0)
          {
           ArrayFree(Trends);
           i_total=0;
           start=C_ZigZag.Total();
          }
        else
           i_total=ArraySize(Trends);
        if(start==0)
           return true;
        break;
     }

之后,从指标类中请求必要数量的极值。

   s_Extremum  base[];
   if(!C_ZigZag.Extremums(base,0,start))
      return false;
   int total=ArraySize(base);
   if(total<=0)
      return true;

如果到该时刻为止数据库中没有极值,请通过调用 AddTrendPoint 方法将最旧的极值添加到数据库。

   if(i_total==0)
      if(!AddTrendPoint(base[total-1]))
         return false;

接下来,安排循环迭代所有下载的极值。 将跳过上次保存的前一个极值。

   for(int i=total-1;i>=0;i--)
     {
      int trends_pos=i_total-1;
      if(Trends[trends_pos].TimeStartBar>=base[i].TimeStartBar)
         continue;

在下一步中,检查极值点是否是单向的。 如果新的极值重新绘制前一个极值,则更新数据。

      if(IsHigh(Trends[trends_pos]))
        {
         if(IsHigh(base[i]))
           {
            if(Trends[trends_pos].Price<base[i].Price)
              {
               Trends[trends_pos].Price=base[i].Price;
               Trends[trends_pos].TimeStartBar=base[i].TimeStartBar;
              }
            continue;
           }

对于相反指向的极值点,检查新走势是否是先前趋势的延续。 如果是,则更新极值数据。 如果不是,则通过调用 AddTrendPoint 方法在极值上添加数据;

         else
           {
            if(trends_pos>1 && Trends[trends_pos-1].Price>base[i].Price  && Trends[trends_pos-2].Price>Trends[trends_pos].Price)
              {
               double trend=fabs(Trends[trends_pos].Price-Trends[trends_pos-1].Price);
               double correction=fabs(Trends[trends_pos].Price-base[i].Price);
               if(fabs(1-correction/trend)>d_MinCorrection)
                 {
                  Trends[trends_pos-1].Price=base[i].Price;
                  Trends[trends_pos-1].TimeStartBar=base[i].TimeStartBar;
                  i_total--;
                  ArrayResize(Trends,i_total);
                  continue;
                 }
              }
            AddTrendPoint(base[i]);
           }
        }

附件中提供了所有类及其方法的完整代码。

3.2. 搜索形态

在定义价格极值之后,构建用于搜索入场点的模块。 将这项工作分为两个子步骤:

  1. 搜索潜在的入场形态。
  2. 入场点。

此功能已分配给 CPttern 类。 它的头部在下面提供。

class CPattern : public CObject
  {
private:
   s_Extremum     s_StartTrend;        //趋势起始点
   s_Extremum     s_StartCorrection;   //调整起始点
   s_Extremum     s_EndCorrection;     //调整结束点
   s_Extremum     s_EndTrend;          //趋势完成点
   double         d_MinCorrection;     //最小调整
   double         d_MaxCorrection;     //最大调整
//---
   bool           b_found;             //"已形态检测" 标志
//---
   CTrends       *C_Trends;
public:
                     CPattern();
                    ~CPattern();
//--- 类初始化
   virtual bool      Create(CTrends *trends, double min_correction, double max_correction);
//--- 搜索形态和入场点的方法
   virtual bool      Search(datetime start_time);
   virtual bool      CheckSignal(int &signal, double &sl, double &tp1, double &tp2);
//--- 比较对象的方法
   virtual int       Compare(const CPattern *node,const int mode=0) const;
//--- 获取形态极值数据的方法
   s_Extremum        StartTrend(void)        const {  return s_StartTrend;       }
   s_Extremum        StartCorrection(void)   const {  return s_StartCorrection;  }
   s_Extremum        EndCorrection(void)     const {  return s_EndCorrection;    }
   s_Extremum        EndTrend(void)          const {  return s_EndTrend;         }
   virtual datetime  EndTrendTime(void)            {  return s_EndTrend.TimeStartBar;  }
  };

使用四个相邻的极值来定义形态。 它们上的数据保存在 s_StartTrend,s_StartCorrection,s_EndCorrection 和 s_EndTrend 结构中。 为了识别形态,我们还需要存储在 d_MinCorrection 和 d_MaxCorrection 变量中的最小和最大调整等级。 我们将从先前创建的 CTrends 类的实例中获取极值。

在类初始化期间,我们将指针传递给 CTrends 类对象和边界调整等级。 在方法内部,检查所传递指针的有效性,保存接收到的信息并清除极值的结构。

bool CPattern::Create(CTrends *trends,double min_correction,double max_correction)
  {
   if(CheckPointer(trends)==POINTER_INVALID)
      return false;
//---
   C_Trends=trends;
   b_found=false;
   s_StartTrend.Clear();
   s_StartCorrection.Clear();
   s_EndCorrection.Clear();
   s_EndTrend.Clear();
   d_MinCorrection=min_correction;
   d_MaxCorrection=max_correction;
//---
   return true;
  }

搜索潜在形态将在 Search() 方法中执行。 参数中的方法接收搜索开始日期,并返回搜索结果形成的逻辑值。 我们来详细考察方法的算法。

首先,检查指向 CTrends 类对象的指针的相关性,以及是否存在已保存的极值。 如果结果为否定,则使用 false 结果退出方法。

bool CPattern::Search(datetime start_time)
  {
   if(CheckPointer(C_Trends)==POINTER_INVALID || C_Trends.Total()<4)
      return false;

接下来,定义与输入中指定的日期对应的极值点。 如果未找到极值,则使用 false 结果退出方法。

   int start=C_Trends.ExtremumByTime(start_time);
   if(start<0)
      return false;

接下来,安排循环从指定日期开始迭代直到最后检测到的所有极值。 首先,我们获取四个连续的极值。 如果连至少一个极值都未有,则移至下一个极值。

   b_found=false;
   for(int i=start;i>=0;i--)
     {
      if((i+3)>=C_Trends.Total())
         continue;
      if(!C_Trends.Extremum(s_StartTrend,i+3) || !C_Trends.Extremum(s_StartCorrection,i+2) ||
         !C_Trends.Extremum(s_EndCorrection,i+1) || !C_Trends.Extremum(s_EndTrend,i))
         continue;

在下一阶段,检查极值是否与必要的形态相对应。 如果它们不对应,则转到下一个极值。 如果检测到形态,将标志设置为 true 并以相同结果退出方法。

      double trend=s_StartCorrection.Price-s_StartTrend.Price;
      double correction=s_StartCorrection.Price-s_EndCorrection.Price;
      double re_trial=s_EndTrend.Price-s_EndCorrection.Price;
      double koef=correction/trend;
      if(koef<d_MinCorrection || koef>d_MaxCorrection || (1-fmin(correction,re_trial)/fmax(correction,re_trial))>=d_MaxCorrection)
         continue;
      b_found= true; 
//---
      break;
     }
//---
   return b_found;
  }

下一步是检测入场点。 我们将使用 第二种案例。 为了降低价格不返回颈线的风险,我们将在较低的时间帧内搜索确认信号。

若要实现此功能,我们创建 CheckSignal() 方法。 除了信号本身,该方法还会返回止损和止盈价位。 所以,我们即将使用方法参数中指向变量的指针。

在该方法的开头,检查是否存在先前检测到形态的标志。 如果未找到形态,则使用 "false" 结果退出方法。

bool CPattern::CheckSignal(int &signal, double &sl, double &tp1, double &tp2)
  {
   if(!b_found)
      return false;

然后,判断形态形成蜡烛的收盘时间,并从形态形成的开始加载我们感兴趣的时间帧的数据直到当前时刻。

   string symbol=C_Trends.Symbol();
   if(symbol=="Not Initilized")
      return false;
   datetime start_time=s_EndTrend.TimeStartBar+PeriodSeconds(C_Trends.Timeframe());
   int shift=iBarShift(symbol,e_ConfirmationTF,start_time);
   if(shift<0)
      return false;
   MqlRates rates[];
   int total=CopyRates(symbol,e_ConfirmationTF,0,shift+1,rates);
   if(total<=0)
      return false;

在那之后,安排循环,我们将逐根柱线检查颈线突破,蜡烛调整,和收盘蜡烛在预期走势方向超越颈线。

我在这里添加了一些限制:

  • 如果价格突破顶部/底部价位,则该形态被视为无效。
  • 如果价格达到预期的止盈价位,则该形态被视为无效。
  • 如果自信号激活以来在开仓之前形成了两根以上的蜡烛,则忽略入场信号。
如果检测到取消形态事件之一,则使用 false 结果退出该方法。

   signal=0;
   sl=tp1=tp2=-1;
   bool up_trend=C_Trends.IsHigh(s_EndTrend);
   double extremum=(up_trend ? fmax(s_StartCorrection.Price,s_EndTrend.Price) : fmin(s_StartCorrection.Price,s_EndTrend.Price));
   double exit_level=2*s_EndCorrection.Price - extremum;
   bool break_neck=false;
   for(int i=0;i<total;i++)
     {
      if(up_trend)
        {
         if(rates[i].low<=exit_level || rates[i].high>extremum)
            return false;
         if(!break_neck)
           {
            if(rates[i].close>s_EndCorrection.Price)
               continue;
            break_neck=true;
            continue;
           }
         if(rates[i].high>s_EndCorrection.Price)
           {
            if(sl==-1)
               sl=rates[i].high;
            else
               sl=fmax(sl,rates[i].high);
           }
         if(rates[i].close<s_EndCorrection.Price || sl==-1)
            continue;
         if((total-i)>2)
            return false;

在检测到入场信号后,指定信号类型(“-1” - 卖出,“1” - 买入)和交易价位。 止损设定在突破后相对于颈线的最大调整深度。 为止盈设置两个价位:

1. 位于极值线到持仓方向颈线的 90%。

2. 位于之前趋势走势的 90%。

添加限制:第一个止盈价位不能超过第二个。

         signal=-1;
         double top=fmax(s_StartCorrection.Price,s_EndTrend.Price);
         tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*0.9;
         tp2=top-(top-s_StartTrend.Price)*0.9;
         tp1=fmax(tp1,tp2);
         break;
        }

附件中提供了所有类和方法的完整代码。

3.3. 开发 EA

准备工作完成后,将所有模块收集到一个 EA 中。 声明外部变量并将它们分成三个模块:

  • 之字折线指标参数;
  • 用于搜索性爱和入场点的参数;
  • 用于执行交易操作的参数。

sinput   string            s1             =  "---- ZigZag Settings ----";     //---
input    int               i_Depth        =  12;                              // 深度
input    int               i_Deviation    =  100;                             // 偏离
input    int               i_Backstep     =  3;                               // 后退步数
input    int               i_MaxHistory   =  1000;                            // 最大历史, 柱线
input    ENUM_TIMEFRAMES   e_TimeFrame    =  PERIOD_M30;                      // 工作时间帧
sinput   string            s2             =  "---- Pattern Settings ----";    //---
input    double            d_MinCorrection=  0.118;                           // 最小修正
input    double            d_MaxCorrection=  0.5;                             // 最大修正
input    ENUM_TIMEFRAMES   e_ConfirmationTF= PERIOD_M5;                       // 确认时间帧
sinput   string            s3             =  "---- Trade Settings ----";      //---
input    double            d_Lot          =  0.1;                             // 交易手数
input    ulong             l_Slippage     =  10;                              // 滑点
input    uint              i_SL           =  350;                             // 止损后退,点数

在全局变量中,声明用于存储指向形态对象的指针数组,交易操作类的实例,形态搜索类的实例,其中存储指向所处理类实例的指针,以及用于存储下一个形态搜索变量的开始时间。

CArrayObj         *ar_Objects;
CTrade            *Trade;
CPattern          *Pattern;
datetime           start_search;

若要启用同时设置两个止盈的功能,请使用文章 [2] 中提供的技术。

在 OnInit() 函数中初始化所有必需的对象。 由于我们从未声明过 CZigZag 和 CTrends 类实例,因此我们只需初始化它们,并将指向这些对象的指针添加到数组中。 如果出现初始化错误,则在任何阶段使用 INIT_FAILED 结果退出该函数。

int OnInit()
  {
//--- 初始化对象数组
   ar_Objects=new CArrayObj();
   if(CheckPointer(ar_Objects)==POINTER_INVALID)
      return INIT_FAILED;
//--- 初始化之字折线指标类
   CZigZag *zig_zag=new CZigZag();
   if(CheckPointer(zig_zag)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(zig_zag))
     {
      delete zig_zag;
      return INIT_FAILED;
     }
   zig_zag.Create(_Symbol,i_Depth,i_Deviation,i_Backstep,e_TimeFrame);
   zig_zag.MaxHistory(i_MaxHistory);
//--- 初始化趋势走势搜索类
   CTrends *trends=new CTrends();
   if(CheckPointer(trends)==POINTER_INVALID)
      return INIT_FAILED;
   if(!ar_Objects.Add(trends))
     {
      delete trends;
      return INIT_FAILED;
     }
   if(!trends.Create(zig_zag,d_MinCorrection))
      return INIT_FAILED;
//--- 初始化交易操作类
   Trade=new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
   Trade.SetAsyncMode(false);
   Trade.SetDeviationInPoints(l_Slippage);
   Trade.SetTypeFillingBySymbol(_Symbol);
//--- 初始化其它变量
   start_search=0;
   CLimitTakeProfit::OnlyOneSymbol(true);
//---
   return(INIT_SUCCEEDED);
  }

清除 OnDeinit() 函数中应用对象的实例。

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(ar_Objects)!=POINTER_INVALID)
     {
      for(int i=ar_Objects.Total()-1;i>=0;i--)
         delete ar_Objects.At(i);
      delete ar_Objects;
     }
   if(CheckPointer(Trade)!=POINTER_INVALID)
      delete Trade;
   if(CheckPointer(Pattern)!=POINTER_INVALID)
      delete Pattern;
  }

像往常一样,主要功能在 OnTick 函数中实现。 它可以划分为两个模块:

1. 检查先前检测到形态中的入场信号。 每当在搜索信号确认的较小时间帧内出现新蜡烛时,都会启动它。

2. 搜索新形态。 每当在工作时间帧(指标指定)内出现新蜡烛时,都会启动它。

在函数开始时,检查入场点确认时间帧内是否存在新柱线。 如果柱线未形成,则退出该函数直到下一次逐笔报价。 应该注意,只有在确认入场点的时间帧不超过工作时间帧时,此方法才能正常工作。 否则,您需要转到形态搜索模块,而非退出该函数。

void OnTick()
  {
//---
   static datetime Last_CfTF=0;
   datetime series=(datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE);
   if(Last_CfTF>=series)
      return;
   Last_CfTF=series;

如果出现新柱线,则安排循环检查所有先前保存的形态是否存在入场信号。 我们不会检查前两个数组对象的信号,因为我们在这些单元中存储指向极值搜索类实例的指针。 如果存储的指针无效或信号检查函数返回 false,则指针将从数组中删除。 在 CheckPattern() 函数中检查形态信号。 其算法将在下面提供。

   int total=ar_Objects.Total();
   for(int i=2;i<total;i++)
     {
      if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID)
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
//---
      if(!CheckPattern(ar_Objects.At(i)))
        {
         if(ar_Objects.Delete(i))
           {
            i--;
            total--;
            continue;
           }
        }
     }

检查先前检测到的形态后,是时候转到第二个模块 — 搜索新形态。 为此,检查工作时间帧内新柱线的可用性。 如果未形成新柱线,则退出等待新的逐笔报价的函数。

   static datetime Last_WT=0;
   series=(datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE);
   if(Last_WT>=series)
      return;

出现新柱线时,定义搜索形态的初始日期(考虑参数中指定的分析历史的深度)。 接下来,检查指向 CPattern 类对象的指针的相关性。 如果指针无效,则创建一个新的类实例。

   start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame)));
   if(CheckPointer(Pattern)==POINTER_INVALID)
     {
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         return;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         return;
        }
     }
   Last_WT=series;

之后,在循环中调用搜索潜在形态的方法。 如果搜索成功,将搜索的开始日期移至新形态,并在先前已发现形态数组中检查是否存在检测到的形态。 如果数组中已存在该形态,则移至新搜索。

   while(!IsStopped() && Pattern.Search(start_search))
     {
      start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame));
      bool found=false;
      for(int i=2;i<ar_Objects.Total();i++)
         if(Pattern.Compare(ar_Objects.At(i),0)==0)
           {
            found=true;
            break;
           }
      if(found)
         continue;

如果找到新形态,则通过调用 CheckPattern() 函数检查入场信号。 之后,如有必要,将形态保存到数组中,并为下一次搜索初始化新的类实例。 循环继续,直到 Search() 方法在某次后续搜索中返回 false

      if(!CheckPattern(Pattern))
         continue;
      if(!ar_Objects.Add(Pattern))
         continue;
      Pattern=new CPattern();
      if(CheckPointer(Pattern)==POINTER_INVALID)
         break;
      if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection))
        {
         delete Pattern;
         break;
        }
     }
//---
   return;
  }

我们看一下 CheckPattern() 函数算法,完成全景。 该方法在参数中接收指向 CPatern 类实例的指针,并返回操作结果的逻辑值。 如果函数返回 false,则从已保存对象的数组中删除所分析的形态。

在函数开始时,调用 CPattern 类的入场信号搜索方法。 如果检查失败,则以 false 结果退出函数。

bool CheckPattern(CPattern *pattern)
  {
   int signal=0;
   double sl=-1, tp1=-1, tp2=-1;
   if(!pattern.CheckSignal(signal,sl,tp1,tp2))
      return false;

如果入场信号搜索成功,则根据信号设置交易价位,并发送入场订单。

   double price=0;
   double to_close=100;
//---
   switch(signal)
     {
      case 1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
        CLimitTakeProfit::Clear();
        if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((tp2-price)/_Point),to_close))
              return false;
        if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,0,NULL))
           return false;
        break;
      case -1:
        price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
        CLimitTakeProfit::Clear();
        if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(CLimitTakeProfit::AddTakeProfit((uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100)))
              to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100);
        if(to_close>0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point)
           if(!CLimitTakeProfit::AddTakeProfit((uint)((price-tp2)/_Point),to_close))
              return false;
        if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,0,NULL))
           return false;
        break;
     }
//---
   return true;
  }

如果开仓成功,则使用 false 结果退出该函数。 这样做是为了从数组中删除使用过的形态。 这允许我们避免在同一形态上重复开仓。

附件中提供了所有方法和函数的完整代码。

4. 策略测试

现在 EA 已经开发完毕,是时候依据历史数据检验其操作了。 该测试将在 2018 年的 9 个月期间进行,品种为 EURUSD。 在 M30 上执行形态搜索,而在 М5 上检测开仓入场点。

测试 EA测试 EA

测试结果显示 EA 有产生利润的能力。 EA 在测试期内进行了 90 笔交易(其中 70 笔是盈利的)。 盈利因子为 2.02,恢复因子为 4.77,表明在实盘账户中使用 EA 的可能性。 完整的测试结果显示如下。

测试结果测试结果

结束语

在本文中,我们基于双顶/双底趋势逆转形态开发了 EA。 依据历史数据测试 EA 已经证明了结果可接受,并且 EA 产生利润的能力证实了在搜索入场点时应用双订/双底形态作为有效趋势逆转信号的可能性。

参考

  1. 在智能交易系统代码中实现指标计算
  2. 使用限价订单替代止盈且无需更改 EA 的原始代码

本文中使用的程序

#
名称
类型
描述
1 ZigZag.mqh 类库 之字折线指标类
2 Trends.mqh  类库 趋势搜索类
3 Pattern.mqh 类库 处理形态的类
4 LimitTakeProfit.mqh 类库 用限价订单替换止盈的类
5 Header.mqh 函数库 EA 头文件
6 DoubleTop.mq5 智能交易系统 基于双顶/双底策略的 EA


本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/5319

附加的文件 |
MQL5.zip (183.66 KB)
跳空缺口 - 是能够获利的策略还是五五开? 跳空缺口 - 是能够获利的策略还是五五开?

这篇文章详细讨论了跳空缺口 — 前一时间段的收盘价和后一时间段的开盘价之间的较大差距, 以及对日柱方向的预测。还探讨了通过系统DLL使用 GetOpenFileName 函数的问题。

使用限价订单替代止盈且无需修改 EA 的原始代码 使用限价订单替代止盈且无需修改 EA 的原始代码

使用限价订单来替代传统的止盈是论坛讨论的长期话题。 这种方法的优点是什么?如何在您的交易中实施? 在本文中,我将向您介绍我对此主题的看法。

反向交易: 减少最大回撤以及在其它市场上测试 反向交易: 减少最大回撤以及在其它市场上测试

在这篇文章中, 我们继续致力于反向交易技巧。我们将会尝试减少最大余额回撤,直到对之前探讨的交易工具可以接受的水平。我们将会看看这样是否将会减少利润,我们还将在其它市场中检验反转方法的运行,包括股票、商品、指数、ETF和农产品市场。注意,本文包含了很多图片!

逆转形态:测试头肩形态 逆转形态:测试头肩形态

本文是前一篇名为“逆转形态:测试双顶/双底形态”的后续文章。 现在我们将会看到另一个著名的逆转形态,称为头肩,比较两种形态的交易效率,并尝试将它们合并成为单一的交易系统。