English Русский Español Deutsch 日本語 Português
形态与示例(第一部分):多顶

形态与示例(第一部分):多顶

MetaTrader 5交易系统 | 23 八月 2021, 16:18
5 216 0
Evgeniy Ilin
Evgeniy Ilin

内容


摘要

形态是互联网上经常讨论的话题,因为有许多交易者使用它们。 形态可转嫁为视觉分析条件,可用来判断随后的定价方向。 算法交易与此不同。 算法交易无法采用视觉条件。 智能交易系统和指标拥有处理价格序列的独特方法。 两端各具优缺点。 代码缺乏人类思维的广度和人类分析的品质,但代码亦有其它宝贵的优势:无与伦比的速度,和无可比拟的单位时间内处理数值或逻辑数据的数量。 指导一台机器做什么并不容易。 这需要一些练习。 随着时间的推移,程序员开始理解机器,而机器也开始理解程序员。 本系列文章对初学者很有用,他们将学到如何组织自己的思路,并将复杂的任务分解为较简单的步骤。


关于逆转形态

就我个人而言,逆转形态的定义过于模糊。 甚而,它们无任何数学根基。 实话实说,任何形态都没有数学根基,故在此唯一可以研讨的数学就是统计学。 统计数据是判断真相的唯一标准,但统计数据是根据真实交易编制的。 显而易见,没有任何来源可提供极其精确的统计数据。 单为某个特定的研究问题提供这样的数据甚至没有意义。 在此,唯一的解决方案是在可视化策略测试器中进行回测。 尽管该方法提供的数据品质较低,但它拥有无可否认的优势,即速度和数据量。 

当然,逆转形态并不能作为判定趋势逆转的充分工具,但结合其它分析方法,例如水平或烛条分析,就可生成期望的结果。 在本系列文章中,并不把形态视为一种特别有趣的分析方法,但它们可用于练习算法交易技巧。 除了练习之外,您还会得到一个有趣且有用的辅助工具 — 即便不用于算法交易,那也可吸引交易者的目光。 有用的指标很受重视。


为什么是多顶 — 其特别之处

处于其简单性,这种形态在网上非常流行。 这种形态在不同的交易工具,和不同的图表时间帧上很常见,因为它并无复杂之处。 甚而,如果您仔细观察该形态,您会发现可由算法交易和 MQL5 语言能力来扩展方法概念。 我们可以尝试创建一些不限于双顶的通用代码。 明晰创建的原型可用于探索所有形态的混合体和后继者。

多重顶部的经典继承者是十分流行的“头肩”形态。 不幸的是,没有关于如何依据这种形态进行交易的结构化信息。 这个问题对于大量流行的策略都很常见 — 因为有很多漂亮的辞藻,却无统计数据支持。 我将尝试在本文中了解,是否可在算法交易的框架中运用它们。 在不用模拟或实盘账户进行交易的情况下收集统计数据的唯一方法就是借助策略测试器的能力。 如果没有此工具,您将无法就特定策略得出任何复杂的结论。


双顶概念能否延展?

关于本文的主题,我将尝试将图表绘制为从双顶开始的形态树。 这将有助于理解这个概念的可能性如何:

树

我决定将若干种基于大致相同思路的推设形态概念结合起来。 这个思路开始很简单 - 在任何方向上找到一段良好走势,并推断它理应逆转的位置。 在与提议的形态建立直观接触后,交易者应能绘制一些辅助线,这有助于评估形态是否符合某些标准,以及判定行情入场点,还有目标和止损价位。 在此可用止盈代替目标价位。

形态可以有一些共同的构造原则,基于这些原则可把这些形态的概念加以组合。 这种清晰定义是算法交易者与人工交易者的不同之处。 对同一原则的不确定性和多重解释,也许会导致令人失望的后果。

基本形态如下:

  1. 双顶
  2. 三重顶
  3. 头肩

这些形态拥有相似的结构和运用原则。 所有这些都旨在识别逆转。 所有三种形态的辅助线都遵循相似的逻辑。 请参考双顶的例子:

双重极值

在上图中,所有必需的线都已编号,含义如下:

  1. 趋势阻力
  2. 定义悲观峰值的辅助线(有人认为是脖子)
  3. 颈线
  4. 乐观目标(它也是交易的获利价位)
  5. 最大允许止损价位(设置在远顶)
  6. 乐观预测线(等于之前的趋势走势)

悲观目标是由相对于离行情最近的边缘颈线交叉点确定的 - 我们取 “1” 和 “2” 之间的距离,表示为“t”,并在提议的逆转方向上测量出相同距离。 判定乐观目标的最小值方法与前类似,但距离在 “5” 和 “3” 之间,用“s”表示。


编写代码来渲染多个顶部

我们首先以合理逻辑来定义这些形态。 为了搜索一个形态,我们应坚持逐根柱线的逻辑,也就是说,我们的操作不依据即时报价,而是遵循柱线。 在这种情况下,它将大大减少终端的负载,因为这样操作可避免不必要的计算。 首先,我们确定一个象征某些独立观察者的类,他们将搜索该形态。 检测正确形态的所有操作都需要作为实例的一部分,故将在其内执行搜索。 我之所以选择这个解决方案,是为了支持进一步的代码修改,例如,当我们需要扩展功能,或修改现有功能之时。

类图映射

我们从研讨类的内容开始:

class ExtremumsPatternFamilySearcher// class simulating an independent pattern search
   {
   private:
   int BarsM;// how many bars on chart to use
   int MinimumSeriesBarsM;// the minimum number of bars in a row to detect a top
   int TopsM;// number of tops in the pattern
   int PointsPessimistM;// minimum distance in points to the nearest target
   double RelativeUnstabilityM;// maximum excess of the head size relative to the minimum shoulder
   double RelativeUnstabilityMinM;// minimum excess of the head size relative to the minimum shoulder
   double RelativeUnstabilityTimeM;// maximum excess of head and shoulders sizes
   bool bAbsolutelyHeadM;// whether a pronounced head is required
   bool bRandomExtremumsM;// random selection of extrema
     


   struct Top// top data
      {
      datetime Datetime0;// time of the candlestick closest to the market
      datetime Datetime1;// time of the next candlestick
      int Index0;// index of the candlestick closest to the market
      int Index1;// index of the next candlestick
      datetime DatetimeExtremum;// time of the top
      int IndexExtremum;// index of the top
      double Price;// price of the top
      bool bActive;// if the top is active (if not, then it does not exist)
      };
   
   struct Line// line
      {
      double Price0;// price of the candlestick closest to the market, to which the line is bound
      datetime Time0;// time of the candlestick closest to the market, to which the line is bound
      double Price1;// price of the farthest candlestick to which the line is bound
      datetime Time1;// time of the farthest candlestick to which the line is bound
      datetime TimeX;// time of the X point
      int Index1;// index of the left edge
      bool DirectionOfFormation;// direction
      double C;// free coefficient in the equation
      double K;// aspect ratio
   
      void CalculateKC()// find unknowns in the equation
         {
         if ( Time0 != Time1 ) K=double(Price0-Price1)/double(Time0-Time1);
         else K=0.0;
         C=double(Price1)-K*double(Time1);
         }
      
      double Price(datetime T)// function of line depending on time
         {
         return K*T+C;
         }
      };
   
   public:   
   
   ExtremumsPatternFamilySearcher(int BarsI,int MinimumSeriesBarsI,int TopsI,int PointsPessimistI, double RelativeUnstabilityI,
   double RelativeUnstabilityMinI,double RelativeUnstabilityTimeI,bool bAbsolutelyHeadI,bool bRandomExtremumsI)// parametric constructor
      {
      BarsM=BarsI;
      MinimumSeriesBarsM=MinimumSeriesBarsI;
      TopsM=TopsI;
      PointsPessimistM=PointsPessimistI;
      RelativeUnstabilityM=RelativeUnstabilityI;
      RelativeUnstabilityMinM=RelativeUnstabilityMinI;
      RelativeUnstabilityTimeM=RelativeUnstabilityTimeI;
      bAbsolutelyHeadM=bAbsolutelyHeadI;
      bRandomExtremumsM=bRandomExtremumsI;
      bPatternFinded=bFindPattern();
      }
      
   int FormationDirection;// direction of the formation (multiple top or bottom, or none at all) ( -1,1,0 )      
   bool bPatternFinded;// if the pattern was found during formation
   Top TopsUp[];// required upper extrema
   Top TopsDown[];// required lower extrema
   Top TopsUpAll[];// all upper extrema
   Top TopsDownAll[];// all lower extrema
   int RandomIndexUp[];// array for the random selection of the tops index
   int RandomIndexDown[];// array for the random selection of the bottoms index
   Top StartTop;// where the formation starts (top farthest from the market)
   Top EndTop;// where the formation ends (top closest to the market)
   Line Neck;// neck
   Top FarestTop;// top farthest from the neck (will be used to determine the head or the formation size) or the same as the head
   Line OptimistLine;// line of optimistic forecast
   Line PessimistLine;// line of pessimistic forecast
   Line BorderLine;// line at the edge of the pattern
   Line ParallelLine;// line parallel to the trend resistance
   
      
   private:
   void SetTopsSize();// setting sizes for arrays with tops
   bool SearchFirstUps();// search for tops
   bool SearchFirstDowns();// search for bottoms
   void CalculateMaximum(Top &T,int Index0,int Index1);// calculate the maximum price between two bars
   void CalculateMinimum(Top &T,int Index0,int Index1);// calculate the minimum price between two bars
   bool PrepareExtremums();// prepare extrema
   bool IsExtremumsAbsolutely();// control the priority of tops
   void DirectionOfFormation();// determine the direction of the formation
   void FindNeckUp(Top &TStart,Top &TEnd);// find neck for the bullish pattern
   void FindNeckDown(Top &TStart,Top &TEnd);// find neck for the bearish pattern
   void SearchFarestTop();// find top farthest from the neck
   bool bBalancedExtremums();// initial balancing of extrema (so that they do not differ much)
   bool bBalancedExtremumsHead();// if a pattern has more than 2 tops, we can check for a pronounced head
   bool bBalancedExtremumsTime();// require that the extrema be not very far in time relative to the minimum distance
   bool bBalancedHead();// balance the head (in other words, require that it be neither the first nor the last one on the list of tops, if there are more than three of them)
   bool CorrectNeckUpLeft();// adjust the neck so as to find the intersection of price and neck (this creates prerequisites for the previous trend) 
   bool CorrectNeckDownLeft();// similarly for the bottom
   int CorrectNeckUpRight();// adjust the neck so as to find the intersection of price and neck on the right or at the current price position, which is the same (to determine the entry point)
   int CorrectNeckDownRight();// similarly for the bottom
   void SearchLineOptimist();// calculate the optimistic forecast line
   bool bWasTrend();// determine whether a trend preceded the pattern definition (in this case the optimistic target line is considered as the trend beginning)
   void SearchLineBorder();// determine trend resistance or support (usually a sloping line)
   void CalculateParallel();// determine a line parallel to support or resistance (crosses the neck at the pattern low or high)
   bool bCalculatePessimistic();// calculate the line of the pessimistic target
   bool bFindPattern();// perform all the above actions
   int iFindEnter();// find intersection with the neck
   public:
   void CleanAll();// clean up objects
   void DrawPoints();// draw points
   void DrawNeck();// draw the neck
   void DrawLineBorder();// line at the border
   void DrawParallel();// line parallel to the border
   void DrawOptimist();// line of optimistic forecast
   void DrawPessimist();// line of pessimistic forecast
   };

一个类,可理解为一个人代替机器的位置执行顺序操作。 无论如何,检测任何编队都可拆分为一组相互衔接的简单操作。 数学中有一条规则:如果你不知道如何解方程,就简化它。 这条规则不仅适用于数学,也适用于任何算法。 首先检测逻辑不很清晰。 但如果您知道从何处开始检测,任务就会变得简单得多。 在这种情况下,为了查找整个形态,我们搜索顶部或底部,或其实都搜索。

确定顶部和底部

若无顶部和底部,则整个形态就毫无意义,因为顶部和底部的存在是形态的必要条件,尽管仅有这个条件还是不够的。 有不同的方法来判定顶部。 最重要的条件是存在明显的半波,而半波是由两个明显的互逆走势决定的,而在我们的例子中应该是有连续几根柱线在一个方向上。 为此目的,我们需要判定一个方向上的最少柱线数,这表明走势存在。 为此,我们提供一个输入变量。 

bool ExtremumsPatternFamilySearcher::SearchFirstUps()// find tops
   {
   int NumUp=0;// the number of found tops
   int NumDown=0;// the number of found bottoms
   bool bDown=false;// an auxiliary boolean which shows if a segment of bearish candlesticks has been found
   bool bUp=false;// an auxiliary boolean which shows if a segment of bullish candlesticks has been found
   bool bNextUp=true;// can we move on to searching for the next top
   bool bNextDown=true;// can we move on to searching for the next bottom
   
   for(int i=0;i<ArraySize(TopsUp);i++)// before search, set all necessary tops to an inactive state
      {
      TopsUp[i].bActive=false;
      }
   for(int i=0;i<ArraySize(TopsUpAll);i++)// before search, set all tops to an inactive state
      {
      if (!TopsUpAll[i].bActive) break;
      TopsUpAll[i].bActive=false;
      }
               
   
   for(int i=0;i<BarsM;i++)
      {
      if ( i+MinimumSeriesBarsM-1 < BarsM )// if remaining bars are enough to determine the extremum and we can start searching for the next top
         {
         if ( bNextUp )// if it is allowed to search for the next top
            {
            bDown=true;
            for(int j=i;j<i+MinimumSeriesBarsM;j++)// determine the first extrema for upper tops
               {
               if ( Open[j]-Close[j] < 0 )// if at least one of the selected candlesticks was upward
                  {
                  bDown=false;
                  break;
                  }
               }
            if ( bDown )
               {
               TopsUpAll[NumUp].Datetime0=Time[i+MinimumSeriesBarsM-1];
               TopsUpAll[NumUp].Index0=i+MinimumSeriesBarsM-1;
               bNextUp=false;
               }
            }        
         }

      if ( MinimumSeriesBarsM+i < BarsM && bDown )// if the remaining bars are enough to determine the second half of the extremum and the previous half has been found
         {
         bUp=true;                  
         for(int j=i;j<MinimumSeriesBarsM+i;j++)//determine further candlesticks in the opposite direction
            {
            if ( Open[j]-Close[j] > 0 )//if at least one of the selected candlesticks was downward
               {
               bUp=false;
               break;
               }
            }
         if ( bUp )
            {
            TopsUpAll[NumUp].Datetime1=Time[i];
            TopsUpAll[NumUp].Index1=i;
            TopsUpAll[NumUp].bActive=true;
            bNextUp=false;
            }   
         } 
      // after that, register the found formation as a top, if it is a top
      if ( bDown && bUp )
         {
         CalculateMaximum(TopsUpAll[NumUp],TopsUpAll[NumUp].Index0,TopsUpAll[NumUp].Index1);// calculate extremum between two bars
         bNextUp=true;
         bDown=false;
         bUp=false;
         NumUp++;
         }
      }
   if ( NumUp >= TopsM ) return true;// if the required number of tops have been found
   else return false;
   }

底部则以相反的方式定义:

bool ExtremumsPatternFamilySearcher::SearchFirstDowns()// find bottoms
   {
   int NumUp=0;
   int NumDown=0;
   bool bDown=false;// an auxiliary boolean which shows if a segment of bearish candlesticks has been found
   bool bUp=false;// an auxiliary boolean which shows if a segment of bullish candlesticks has been found
   bool bNextUp=true;// can we move on to searching for the next top
   bool bNextDown=true;// can we move on to searching for the next bottom

   for(int i=0;i<ArraySize(TopsDown);i++)// before search, set all necessary bottoms to an inactive state
      {
      TopsDown[i].bActive=false;
      }
   for(int i=0;i<ArraySize(TopsDownAll);i++)// before search, set all bottoms to an inactive state
      {
      if (!TopsDownAll[i].bActive) break;
      TopsDownAll[i].bActive=false;
      }

   for(int i=0;i<BarsM;i++)
      {
      if ( i+MinimumSeriesBarsM-1 < BarsM )// if remaining bars are enough to determine the extremum and we can start searching for the next top
         {
         if ( bNextDown )// if it is allowed to search for the next bottom
            {
            bUp=true;               
            for(int j=i;j<i+MinimumSeriesBarsM;j++)// determine the first extrema for upper tops
               {
               if ( Open[j]-Close[j] > 0 )//if at least one of the selected candlesticks was downward
                  {
                  bUp=false;
                  break;
                  }
               }
            if ( bUp )
               {
               TopsDownAll[NumDown].Datetime0=Time[i+MinimumSeriesBarsM-1];
               TopsDownAll[NumDown].Index0=i+MinimumSeriesBarsM-1;
               bNextDown=false;
               }
            }        
         }

      if ( MinimumSeriesBarsM+i < BarsM && bUp )// if the remaining bars are enough to determine the second half of the extremum and the previous half has been found
         {   
         bDown=true;                              
         for(int j=i;j<MinimumSeriesBarsM+i;j++)//determine further candlesticks in the opposite direction
            {
            if ( Open[j]-Close[j] < 0 )// if at least one of the selected candlesticks was upward
               {
               bDown=false;
               break;
               }
            }
         if ( bDown )
            {
            TopsDownAll[NumDown].Datetime1=Time[i];
            TopsDownAll[NumDown].Index1=i;
            TopsDownAll[NumDown].bActive=true;
            bNextDown=false;              
            }
         } 
      // after that, register the found formation as a bottom, if it is a bottom
      if ( bDown && bUp )
         {
         CalculateMinimum(TopsDownAll[NumDown],TopsDownAll[NumDown].Index0,TopsDownAll[NumDown].Index1);// calculate extremum between two bars
         bNextDown=true;
         bDown=false;
         bUp=false;            
         NumDown++;
         }
      }
      
   if ( NumDown == TopsM ) return true;//if the required number of bottoms have been found
   else return false;
   }

在这种情况下,我没有采用分形逻辑。 代之,我创建了自己的逻辑来判定顶部和底部。 我不认为它比分形更好或更差,但至少不需要用到任何外部功能。 甚而,无需调用不必要的内置语言函数,因为大多时候根本不需要它们。 这些函数可能很棒,但在这种情况下它们就是多余的。 该函数判定所有顶部和底部,我们会在将来用到它们。 下图直观展示了该函数中发生的情况:

搜索顶部和底部

首先,它搜索走势 1;然后它搜索走势 2;最后走势 3 意味着顶部或底部已被判定。 针对这 3 步的逻辑已在两个独立的函数中实现,如下所示:

void ExtremumsPatternFamilySearcher::CalculateMaximum(Top &T,int Index0,int Index1)// if 2 intermediate points are found, find High between them
   {
   double MaxValue=High[Index0];
   datetime MaxTime=Time[Index0];
   int MaxIndex=Index0;
   for(int i=Index0;i<=Index1;i++)
      {
      if ( High[i] >  MaxValue )
         {
         MaxValue=High[i];
         MaxTime=Time[i];
         MaxIndex=i;
         }
      }
   T.DatetimeExtremum=MaxTime;
   T.IndexExtremum=MaxIndex;
   T.Price=MaxValue;
   }
   
void ExtremumsPatternFamilySearcher::CalculateMinimum(Top &T,int Index0,int Index1)//if 2 intermediate points are found, find Low between them
   {
   double MinValue=Low[Index0];
   datetime MinTime=Time[Index0];
   int MinIndex=Index0;
   for(int i=Index0;i<=Index1;i++)
      {
      if ( Low[i] <  MinValue ) 
         {
         MinValue=Low[i];
         MinTime=Time[i];
         MinIndex=i;
         }
      } 
   T.DatetimeExtremum=MinTime;
   T.IndexExtremum=MinIndex;
   T.Price=MinValue;      
   }

然后,将所有这些都放入预先准备好的容器当中。 逻辑如下:类内部用到的所有结构都需要逐渐附加数据。 经过所有步骤和阶段后,输出所需的数据。 使用这些数据,可在图表上以图形方式显示形态。 当然,顶部和底部的判定逻辑可以不同。 我的目的只是展示复杂事物的简单检测逻辑。

选择要操控的顶部

我们已发现的顶部和底部只是个过渡。 找到它们后,我们需要选择我们认为最适合充当肩部的顶部。 我们无法判定这一点,因为代码无法像人类一样由机器通过视觉识别(一般来说,这种复杂技术的运用并不太可能令绩效受益)。 对于当下,我们选择最贴近行情的顶部:

bool ExtremumsPatternFamilySearcher::PrepareExtremums()// assign the tops with which we will work
   {
   int Quantity;// an auxiliary counter for random tops
   int PrevIndex;// an auxiliary index for maintaining the order of indexes (increment only)
   
   for(int i=0;i<TopsM;i++)// simply select the tops that are closest to the market
      {
      TopsUp[i]=TopsUpAll[i];
      TopsDown[i]=TopsDownAll[i];
      }
   return true;   
   }

从品种图表上可见,逻辑将等同于紫色框架中的变体。 我将绘制更多变体以供选择:

选择顶部和底部

在这种情况下,选择逻辑非常简单。 所选变体为 0 和 1,因为它们最贴近行情。 此处一切都适用于双顶。 但相同逻辑将用于三重或更多重的顶部,唯一的区别在于所选顶部的数量。

此函数在未来会得到扩展,以便启用随机选择顶部的能力,如上图中的蓝色所示。 这将模拟形态查找器的多个实例。 这允许在自动模式下更有效和更频繁地查找所有形态。

判定形态方向

一旦我们识别出顶部和底部,如果在行情的给定点存在这样的形态,我们必须判定形态的方向。 在这个阶段,我考虑为最贴近行情的极值类型方向分配更大的优先级。 基于这个逻辑,我们选用图中的变体0,因为最贴近行情的是底部,而不是顶部(前提是行情上的情况与图中完全相同)。 代码中的这部分很简单:

void ExtremumsPatternFamilySearcher::DirectionOfFormation()// determine whether it is a double top (1) or double bottom (-1) (only if all tops and bottoms are found - if not found, then 0)
   {
   if ( TopsDown[0].DatetimeExtremum > TopsUp[0].DatetimeExtremum && TopsDown[ArraySize(TopsDown)-1].bActive )
      {
      StartTop=TopsDown[ArraySize(TopsDown)-1];
      EndTop=TopsDown[0];    
      FormationDirection=-1;
      }
   else if ( TopsDown[0].DatetimeExtremum < TopsUp[0].DatetimeExtremum && TopsUp[ArraySize(TopsUp)-1].bActive )
      {
      StartTop=TopsUp[ArraySize(TopsUp)-1];
      EndTop=TopsUp[0]; 
      FormationDirection=1;  
      }
   else FormationDirection=0;   
   }

进一步的行动需要明确判定的方向。 方向相当于形态类型:

  1. 多重顶
  2. 多重底

这些规则也适用于头肩形态,和所有其它混合形态。 该类应该对这个家族的所有形态都是通用的 — 这种普遍性已经部分起作用了。

过滤器会舍弃不正确的形态:

现在,我们来更进一步。. 已知我们有一个方向和选择顶部和底部的方法之一,我们必须为多重顶提供以下规则:所选顶部之间应有一个顶部低于所选顶部的最低那个。 对于多重底,则中间底部应高于所选底部中最高的那个。 在这种情况下,如果顶部是随机选择的,则可以清楚地区分所有选定的顶部。 否则,就不需要此检查:

bool ExtremumsPatternFamilySearcher::IsExtremumsAbsolutely()// require the selected extrema to be the most extreme ones
   {
   if ( bRandomExtremumsM )// check only if we have a random selection of tops (in other case the check should be considered completed)
      {
      if ( FormationDirection == 1 )
         {
         int StartIndex=RandomIndexUp[0];
         int EndIndex=RandomIndexUp[ArraySize(RandomIndexUp)-1];
         for(int i=StartIndex+1;i<EndIndex;i++)// check all tops between the selected ones
            {
            for(int j=0;j<ArraySize(TopsUp);j++)
               {
               if ( TopsUpAll[i].Price >= TopsUp[j].Price )
                  {
                  for(int k=0;k<ArraySize(RandomIndexUp);k++)
                     {
                     if ( i != RandomIndexUp[k] ) return false;
                     }
                  }
               }
            }
         return true;
         }
      else if ( FormationDirection == -1 )
         {
         int StartIndex=RandomIndexDown[0];
         int EndIndex=RandomIndexDown[ArraySize(RandomIndexDown)-1];
         for(int i=StartIndex+1;i<EndIndex;i++)// check all tops between the selected ones
            {
            for(int j=0;j<ArraySize(TopsDown);j++)
               {
               if ( TopsDownAll[i].Price <= TopsDown[j].Price )
                  {
                  for(int k=0;k<ArraySize(RandomIndexDown);k++)
                     {
                     if ( i != RandomIndexDown[k] ) return false;
                     }
                  }
               }
            }
         return true;      
         }
      else return false;      
      }
   else
      {
      return true;
      }
   }

如果我们执行最后的辨析函数随机选择顶部,并直观地显示正确或错误变体,它将如下所示:

不明确顶部的控制


这些准则折射出看涨和看跌形态。 图例展示的示例是看涨形态。 第二种情况很容易想象到。

所有准备过程均已完毕,我们可以继续寻找颈部。 不同的交易者会按不同的方式绘制颈部图。 我有条件地判定若干种构造类型:

  1. 视觉上,倾斜(不是阴影)
  2. 视觉上,水平(不是阴影)
  3. 最高点或最低点,倾斜(依据投影)
  4. 最高点或最低点,水平(依据投影)

出于安全原因,并提升盈利机会,我认为最佳变体是 4。 我之所以选择这个,是出于以下几点:

  • 更清晰地发现逆转走势的开始
  • 这种方法更容易在代码中实现
  • 坡度能明确判定的(水平)

也许,从构造的角度来看,这并不完全正确,但我还没有找到任何更明确的规则。 从算法交易的角度来看,这并不严重。 如果我们在这个形态中找到一些合理的东西,测试器或视觉上肯定会向我们展现一些迹象。 进一步的任务在于加强交易结果,不过这是一项截然不同的任务。

我已为看涨和看跌形态创建了两个镜像函数,它们定义了颈部的所有必要参数:

void ExtremumsPatternFamilySearcher::FindNeckUp(Top &TStart,Top &TEnd)// find the neck line based on the two extreme tops (for the classic multiple top)
   {
   double PriceMin=Low[TStart.IndexExtremum];
   datetime TimeMin=Time[TStart.IndexExtremum];
   for(int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)// define the lowest point
      {
      if ( Low[i] < PriceMin )
         {
         PriceMin=Low[i];
         TimeMin=Time[i];
         }
      }
   // define the parameters of the anchor point and all parameters of the line equation
   Neck.Price0=PriceMin;
   Neck.TimeX=TimeMin;
   Neck.Time0=Time[0];
   Neck.Price1=PriceMin;
   Neck.Time1=TStart.DatetimeExtremum;
   Neck.DirectionOfFormation=true;
   Neck.CalculateKC();
   }
   
void ExtremumsPatternFamilySearcher::FindNeckDown(Top &TStart,Top &TEnd)// find the neck line based on two extreme bottoms (for the classic multiple bottom)
   {
   double PriceMax=High[TStart.IndexExtremum];
   datetime TimeMax=Time[TStart.IndexExtremum];
   for(int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)// define the lowest point
      {
      if ( High[i] > PriceMax )
         {
         PriceMax=High[i];
         TimeMax=Time[i];         
         }
      }
   // define the parameters of the anchor point and all parameters of the line equation
   Neck.Price0=PriceMax;
   Neck.TimeX=TimeMax;
   Neck.Time0=Time[0];
   Neck.Price1=PriceMax;
   Neck.Time1=TStart.DatetimeExtremum;
   Neck.DirectionOfFormation=false;
   Neck.CalculateKC();
   }

为了正确并简单地绘制颈部,构造颈部时选用的所有形态家族最好依据相同的规则。 一方面,这能消除了不必要的细节,在我们的例子中什么也没有给出。 为了构建任意复杂度多重顶的颈部,最好使用形态的两个极值顶。 这些峰值的索引将作为我们在所选行情中搜索最低或最高价格的索引。 颈部将是一条常规水平线。 第一个锚点应该正好出于这个水平,而最佳锚点时间应正好等于极值顶部或底部的时间(取决于我们正在参考的形态)。 这就是它在图片中的样子:

颈部

搜索低点或高点的窗口正好在第一个和最后一个顶部之间。 此规则适用于该家族的任何形态,包括任意数量的顶部和底部。

为了判定乐观目标,您首先应定义形态大小。 形态大小是从头到颈部的垂直距离,以点为单位。 为了判定距离,我们首先需要找到离颈部最远的顶部。 这个顶部将作为形态的边界:

void ExtremumsPatternFamilySearcher::SearchFarestTop()// define the farthest top
   {
   double MaxTranslation;// temporary variable to determine the highest top
   if ( FormationDirection == 1 )// if we deal with a multiple top
      {
      MaxTranslation=TopsUp[0].Price-Neck.Price0;// temporary variable to determine the highest top
      FarestTop=TopsUp[0];
      for(int i=1;i<ArraySize(TopsUp);i++)
         {
         if ( TopsUp[i].Price-Neck.Price0 > MaxTranslation ) 
            {
            MaxTranslation=TopsUp[i].Price-Neck.Price0;
            FarestTop=TopsUp[i];
            }
         }      
      }
   if ( FormationDirection == -1 )// if we deal with a multiple bottom
      {
      MaxTranslation=Neck.Price0-TopsDown[0].Price;// temporary variable to determine the lowest bottom
      FarestTop=TopsDown[0];      
      for(int i=1;i<ArraySize(TopsDown);i++)
         {
         if ( Neck.Price0-TopsDown[i].Price > MaxTranslation ) 
            {
            MaxTranslation=Neck.Price0-TopsDown[0].Price;
            FarestTop=TopsDown[i];
            }
         }      
      }
   }

需要额外检查来确保顶部不会有太大差异。 仅当检查成功,我们才能进一步运作。 更精确地说,应该有两次检查:一次检查极值的垂直大小,另一次检查水平(时间)。 如果顶部在时间上太久远,那这种变体也不适合。 此处检查垂直大小:

bool ExtremumsPatternFamilySearcher::bBalancedExtremums()// balance the tops
   {
   double Lowest;// the lowest top for the multiple top
   double Highest;// the highest bottom for the multiple bottom
   double AbsMin;// distance from the neck to the nearest top
   if ( FormationDirection == 1 )// for the multiple top
      {
      Lowest=TopsUp[0].Price;
      for(int i=1;i<ArraySize(TopsUp);i++)// find the lowest top
         {
         if ( TopsUp[i].Price < Lowest ) Lowest=TopsUp[i].Price;
         }
      AbsMin=Lowest-Neck.Price0;// determine distance from the lowest top to the neck
      if ( AbsMin == 0.0 ) return false;
      if ( ((FarestTop.Price - Neck.Price0)-AbsMin)/AbsMin >= RelativeUnstabilityM ) return false;// if the head is too much bigger than the lowest leverage
      }
   else if ( FormationDirection == -1 )// for the multiple bottom
      {
      Highest=TopsDown[0].Price;
      for(int i=1;i<ArraySize(TopsDown);i++)// find the highest top
         {
         if ( TopsDown[i].Price > Highest ) Highest=TopsDown[i].Price;
         }
      AbsMin=Neck.Price0-Highest;// determine distance from the highest top to the neck
      if ( AbsMin == 0.0 ) return false;
      if ( ((Neck.Price0-FarestTop.Price)-AbsMin)/AbsMin >= RelativeUnstabilityM ) return false;// if the head is too much bigger than the lowest leverage
      }
   else return false;
   return true;   
   }

为了判定顶部的正确垂直尺寸,我们需要两个顶部。 第一个是离颈部最远的,第二个是离颈部最近的。 如果两者大小相差太大,那么这个阵型可能会被证明是无效的,最好不要冒险,并将其标记为无效。 与前面的辨析类似,所有这些对应图形都可能伴随着正确和错误:

垂直大小的控制

它们很容易依据视觉来判定,但代码就需要一个量化指标。 在这种情况下,它很简单如下:

  • K = (Max - Min)/Min
  • K <= RelativeUnstabilityM

该指标能非常有效地过滤掉大量假形态。 好吧,即便是最复杂的代码也不能比我们的眼睛更有效率。 我们唯一能做的就是让逻辑尽可能地接近现实 — 但在这里我们必须知道在何处停步。

水平检查看起来很与此相似。 唯一的区别是我们采用柱线索引作为大小(您可以采用时间,其实没有根本区别):

bool ExtremumsPatternFamilySearcher::bBalancedExtremumsTime()// balance the sizes of shoulders and head along the horizontal axis
   {
   double Lowest;// minimum distance between the tops
   double Highest;// maximum distance between the tops
   if ( FormationDirection == 1 )// for the multiple top
      {
      Lowest=TopsUp[1].IndexExtremum-TopsUp[0].IndexExtremum;
      Highest=TopsUp[1].IndexExtremum-TopsUp[0].IndexExtremum;
      for(int i=1;i<ArraySize(TopsUp)-1;i++)// find the lowest top
         {
         if ( TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum < Lowest ) Lowest=TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum;
         if ( TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum > Highest ) Highest=TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum;
         }
      if ( double(Highest-Lowest)/double(Lowest) > RelativeUnstabilityTimeM ) return false;// if the width of one of the waves differs much
      }
   else if ( FormationDirection == -1 )// for the multiple bottom
      {   
      Lowest=TopsDown[1].IndexExtremum-TopsDown[0].IndexExtremum;
      Highest=TopsDown[1].IndexExtremum-TopsDown[0].IndexExtremum;
      for(int i=1;i<ArraySize(TopsDown)-1;i++)// find the lowest top
         {
         if ( TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum < Lowest ) Lowest=TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum;
         if ( TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum > Highest ) Highest=TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum;
         }
      if ( double(Highest-Lowest)/double(Lowest) > RelativeUnstabilityTimeM ) return false;// if the width of one of the waves differs much 
      }
   else return false;
   return true;
   }

对于这个检查,我们可以采用类似的指标。 在视觉上,它可以表示如下:

水平大小的控制

在这种情况下,定量标准将是相同的。 但是,这次我们采用索引,或时间来替代点数。 也许分别实现我们所需比较的数字更佳,这样可为灵活调整保留空间:

  • K = (Max - Min)/Min
  • K <= RelativeUnstabilityTimeM

颈线必须穿过左侧的价格 — 这意味着该形态之前曾有一个趋势:

bool ExtremumsPatternFamilySearcher::CorrectNeckUpLeft()// next the neck line must be corrected so that it finds an intersection with the price on the left
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( Neck.DirectionOfFormation )// if the neck is found for a double top
      {
      for(int i=StartTop.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( High[i] >= FarestTop.Price )// if the movement goes beyond the formation, then the formation is fake
            {
            return false;
            }         
         if ( Close[i] < Neck.Price0 && Open[i] < Neck.Price0 && High[i] < Neck.Price0 && Low[i] < Neck.Price0   )
            {
            Neck.Time1=Time[i];
            Neck.Index1=i;
            return true;
            }
         }
      }
   return false;
   }
   
bool ExtremumsPatternFamilySearcher::CorrectNeckDownLeft()// next the neck line must be corrected so that it finds an intersection with the price on the left
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( !Neck.DirectionOfFormation )// if the neck is found for a double bottom
      {
      for(int i=StartTop.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( Low[i] <= FarestTop.Price )//  if the movement goes beyond the formation, then the formation is fake
            {
            return false;
            }         
         if ( Close[i] > Neck.Price0 && Open[i] > Neck.Price0 && High[i] > Neck.Price0 && Low[i] > Neck.Price0 )
            {
            Neck.Time1=Time[i];
            Neck.Index1=i;
            return true;
            }
         }
      }
   return false;
   }

同样,看涨和看跌形态有两个镜像函数。 下面是这个两个辨析的图解说明:

左右交叉控制

蓝框标记了行情段落中我们控制的交叉点。 两个线段都在形态后面,在极值顶部的左侧和右侧。 

只剩下两个检查:

  1. 我们需要一个在当前时刻(在零号烛条处)穿过颈线的形态
  2. 形态之前必须存在大于或等于形态本身的走势

算法交易需要第一点。 我不认为仅为查看它们而检测阵型是值得的,尽管也提供了这个函数。 我们需要检测并准确找到我们可以交易的点位 — 一旦知道我们处于入场点,我们就可立即开仓。 第二点是必要条件之一,因为如果没有前期的良好走势,形态本身就毫无用处。

判定零号烛条交叉(检查右侧的交叉点)如下:

int ExtremumsPatternFamilySearcher::CorrectNeckUpRight()// next the neck line must be corrected so that it finds an intersection with the price on the right
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( Neck.DirectionOfFormation )// if the neck is found for a double top
      {
      for(int i=EndTop.IndexExtremum;i>1;i--)// define the intersection point
         {
         if ( High[i] > FarestTop.Price || Low[i] < Neck.Price0 )// if the movement goes beyond the formation, then the formation is fake
            {
            return -1;
            }         
         }
      }
      
   if ( Close[0] <= Neck.Price0 )
      {
      Neck.Time0=Time[0];
      return 1;
      }      
   return 0;
   }

int ExtremumsPatternFamilySearcher::CorrectNeckDownRight()// next the neck line must be corrected so that it finds an intersection with the price on the right
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( !Neck.DirectionOfFormation )// if the neck is found for a double bottom
      {
      for(int i=EndTop.IndexExtremum;i>1;i--)// define the intersection point
         {
         if ( Low[i] < FarestTop.Price || High[i] > Neck.Price0  )// if the movement goes beyond the formation, then the formation is fake
            {
            return -1;
            }         
         }
      }
      
   if ( Close[0] >= Neck.Price0 )
      {
      Neck.Time0=Time[0];
      return 1;
      }   
      
   return 0;
   }
<

再一次,我们拥有两个镜像函数。 请注意,如果价格超出形态然后又折返,则右侧的交叉点不被视为有效 - 这种行为已在此处覆盖,并在上图中示意。

现在,我们来判定如何找到之前的趋势。 迄今为止,我正在为此目的而采用乐观预测线。 如果在颈线和乐观预测线之间有一个行情段落,那么这就是所期望走势。 这个走势的延伸时间不能太长,否则它明显不是走势:

bool ExtremumsPatternFamilySearcher::bWasTrend()// did we find the movement preceding the formation (also move here the anchor point to the intersection)
   {
   bool bCrossOptimist=false;// denotes if the neck is crossed
   if ( FormationDirection == 1 )// if the optimistic forecast is at the double top
      {
      for(int i=Neck.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( High[i] > Neck.Price0 )// if the movement goes beyond the neck, then the formation is fake
            {
            return false;
            }         
         if ( Low[i] < OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            return true;
            }
         }
      }
   else if ( FormationDirection == -1 )// if the optimistic forecast is at the double bottom
      {
      for(int i=Neck.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( Low[i] < Neck.Price0 )//  if the movement goes beyond the neck, then the formation is fake
            {
            return false;
            }         
         if ( High[i] > OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            return true;
            }
         }      
      }
   return false;
   }

最后一个辨析也可以用图形表示如下:

先前的走势

我们在此完成代码审查,并继续直观评估。 我认为本文已经充分讲述了该方法的主要思路。 深入的思路将在本系列的下一篇文章中研讨。

我们在 MetaTrader 5 可视测试器中检查结果:

我偏爱在图表上绘制线条,因为它快速、简单且清晰。 MQL5 帮助提供了运用所有图形对象(包括线)的示例。 在此我就不提供绘制代码了,但您可以看看它的执行结果。 当然,一切都可以做得更好,但我们只有一个原型。 所以,我相信在此我们可以运用“必要且充分”原则:

MetaTrader 5 策略测试器可视化工具中的三重顶

这是一个三重顶的例子。 这个例子对我来说似乎更有趣。 可以类似的方式检测双顶 — 您只需在参数中设置所需的顶部数量。 代码不会经常找到这样的阵型,但这只是一个演示。 代码可以进一步微调(我打算稍后再做)。


进一步的发展思路

稍后,我们将研讨本文中未提及的内容,并将提高所有阵型的搜索质量。 我们还将微调这个类,令其能够检测头肩形态。 我们还将尝试寻找这些阵型可能的混合功能; 其中之一可能是“N 个顶部和多个肩部”。 该系列不仅致力于这个形态家族,还将包括新的、有趣和有用的素材。 形态搜索有不同的方法,本系列的思路是运用不同的示例展示尽可能多的形态,从而涵盖将复杂任务尽可能分解为一组更简单任务的不同方法。 该系列将包括:

  1. 其它有趣的形态
  2. 检测不同阵型类别的其它方法
  3. 依据历史数据进行交易,并收集不同金融产品和时间帧的统计数据
  4. 有很多形态,我并不了解所有(所以我大有可能会研究您的形态)
  5. 我们还将研究等级(因为等级通常用于检测逆转)


    结束语

    我试图令资料简单易懂,让每个人都能理解。 我希望任何人都能在这里找到有用的东西。 这篇特别文章的结论是,从可视化策略测试器中可以看出,一套简单的代码也能够找到复杂的形态。 因此,我们不一定非要用到神经网络,或编写/运用一些复杂的机器视觉算法。 MQL5 语言拥有丰富的功能,即便更复杂的算法亦可实现。 可能性仅受限于您的想象力和勤奋。 

    本文由MetaQuotes Ltd译自俄文
    原文地址: https://www.mql5.com/ru/articles/9394

    附加的文件 |
    Prototype.zip (309.42 KB)
    优秀程序员(第 01 部分):您必须停止做这 5 件事才能成为一名成功的 MQL5 程序员 优秀程序员(第 01 部分):您必须停止做这 5 件事才能成为一名成功的 MQL5 程序员
    萌新甚至高级程序员都会有很多坏习惯,这令他们无法在其编程事业中成为最佳的。 我们将在本文中就这些问题予以讨论并定位。 对于所有梦想成为优秀 MQL5 开发者的人来说,这篇文章都是必读的。
    DoEasy 函数库中的图形(第七十八部分):函数库中的动画原理。 图片切分 DoEasy 函数库中的图形(第七十八部分):函数库中的动画原理。 图片切分
    在本文中,我将定义会在函数库某些部分中用到的动画原理。 我还将开发一个类,复制图像的一部分,并将其粘贴到会话窗对象上的指定位置,它是为了保留和恢复叠加图像位置的会话窗背景部分。
    针对交易的组合数学和概率论(第一部分):基础知识 针对交易的组合数学和概率论(第一部分):基础知识
    在本系列文章中,我们将尝试找寻概率论的实际运用来描述交易和定价过程。 在首篇文章中,我们将研究组合数学和概率论的基础知识,并将分析如何在概率论的框架中应用分形的第一个例子。
    DoEasy 函数库中的图形(第七十七部分):阴影对象类 DoEasy 函数库中的图形(第七十七部分):阴影对象类
    在本文中,我将为阴影对象创建一个单独类,它是图形元素对象的衍生后代,并加入渐变填充对象背景的功能。