English Русский Español Deutsch 日本語 Português
研究烛条分析技术(第四部分):形态分析器的更新和补充

研究烛条分析技术(第四部分):形态分析器的更新和补充

MetaTrader 5交易系统 | 17 五月 2019, 09:25
4 449 2
Alexander Fedosov
Alexander Fedosov

目录

概述

在本系列的早期文章中,我们创建了一个 MetaTrader 5 应用程序,该应用程序能够测试现有烛条形态的相关性。 后续的版本可以根据简单的烛条类型创建自定义形态,例如长/短实体蜡烛,十字星,顶纺锤,等等。 在最新的部分,我们开发了一个函数库,可根据烛条形态创建指标和智能交易系统。

本文论述了形态分析器(Pattern Analyzer)应用程序的新版本。 此版本修复了已发现错误并提供了一些新功能,还改进了用户界面。 在新版本的开发过程中参考了上一篇文章中的意见和建议。 最终的应用程序会在本文中进行说明。

更新概述

用户界面是构成任何应用程序的重要组成部分:精心打造的界面结构令应用程序的操作更加高效。 我们将比较新一版与前一版应用程序的外观。 我们从 “Analysis” 选项卡开始:为什么需要改进。

图例 1 前一版本中 “Analysis” 选项卡的界面

第 1 点。 选卡排列和维度。

在图例 1 中,标记为 1 的选卡位于窗口的上部。 该处的右上角部分有大片未利用的空白,然而该区域不足以添加更多选项卡。 文字的字体太小。 这三个选卡已移至窗口的左侧部分:它们现在垂直排列,并且更加明显。 甚或,还有额外空间可以添加更多部分。

第 2 点。 含有形态测试结果的表格。

数据直观呈现的效率不高。 所以,为了更佳的可读性,增加了字号、行高和表格大小。

第 3 点。 当前时间帧选择。

所有形态的选择结构 “Timeframe -> Result” 限制了测试结果的直观显示。 为了改善这一点,我们将开发一个多时间帧选择选项,以及独立的分析形态选择。 这能够更灵活地定制形态操作。 

第 4 点。 样本范围。

在之前版本中实现的思路:自当前数据到历史中一定数量的烛条,在此范围内进行测试。 而无法指定从一个日期到另一个日期的选择。 因此,将修改范围选择方法。 下面的图例 2 描述了所有上述问题和可能的改进解决方案。

图例 2. 更新后的 “Analyze” 选项卡的界面。

上述要点的解决方案如下。

  • 窗口左侧的选卡垂直排列。
  • 手动选择分析形态。
  • 手动选择当前时间帧。
  • 用新的“日期范围”工具选择测试范围,替代原本的烛条数量。

应用程序窗口变得更大,以便显示新元素。 另一个重要的新功能是 'Trend threshold value' 点数参数(图例 3),已从 “Settings” 选卡移到 “Analyze” 和 “AutoSearch” 选项卡。 每个选项卡的设置都是独立的。 


图例 3 趋势阈值参数的新位置

最后一个更改的元素是结果表格的结构。 删除了 “occurrence” 列,且用更相关的 “Timeframe” 参数来替代。 

图例 4. 结果表格的新结构

现在我们看一下第二个选项卡 “AutoSearch” 的改进,它可以操控生成的形态。

第 1 点。 不同选卡中的设置。

与 “AutoSearch” 部分直接相关的设置位于 “Setting” 选项卡中,因此要在 “AutoSearch” 和 “Setting” 选项卡之间不断切换以便修改设置。 这就是为何将几乎所有设置都移至 “AutoSearch” 选项卡的原因。 针对趋势阈值、当前时间帧和日期范围的选择,已经实现了进一步的改进。 “AutoSearch” 选项卡更新后的结果如图例 5 所示。

图例 5 “AutoSearch” 选项卡中的更新功能 

 这样可以更便捷地处理形态。 此选项卡中的日期范围也是独立的。


更新的实现

我们更详细地考察上述更新的实现,以及计算的变化。

应用程序窗口的结构。 主窗口创建方法。

已经补充了负责创建图形界面的 CProgram::CreateGUI()方法:

//+------------------------------------------------------------------+
//| 创建程序的图形界面                                                    |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- 创建面板
   if(!CreateWindow("Pattern Analyzer"))
      return(false);
//--- 创建对话窗口
   if(!CreateWindowSetting1("Settings"))
      return(false);
//--- 创建对话窗口
   if(!CreateWindowSetting2("Date range settings"))
      return(false);
//--- 创建对话窗口
   if(!CreateWindowSetting3("Date range settings"))
      return(false);
//--- 完成 GUI 创建
   CWndEvents::CompletedGUI();
   return(true);
  }
//+-----------------------------------------------------------------

CreateWindowSetting2() 和 CreateWindowSetting3() 负责显示图例 1 所示的新日期范围选择工具。 主应用程序窗口创建方法 CreateWindow() 也已重新设计。 它被划分为三个板块,分别对应于每个选项卡的 UI 元素:Analyze,AutoSearch 和 Setting。

//+------------------------------------------------------------------+
//| “Analyze”选卡                                                      |
//+------------------------------------------------------------------+
//--- 创建形态集的按钮
   if(!CreatePatternSet(m_patterns,10,10))
      return(false);
//--- 时间帧标题
   if(!CreateTFLabel(m_text_labels[1],10,100,0))
      return(false);
//--- 创建时间帧集合的按钮
   if(!CreateTimeframeSet(m_timeframes,10,125,0))
      return(false);
//--- 品种过滤搜索窗口 
   if(!CreateSymbolsFilter(m_symb_filter1,m_request1,10,180,0))
      return(false);
//--- 创建日期范围选择按钮
   if(!CreateDateRange(m_request3,280,180,0))
      return(false);
//--- 为盈利阈值创建输入字段
   if(!CreateThresholdValue(m_threshold1,400,180,100,0))
      return(false);
//--- 创建品种表格
   if(!CreateSymbTable(m_symb_table1,10,225,0))
      return(false);
//--- 创建结果表格
   if(!CreateTable1(m_table1,120,225,0))
      return(false);

在第一个选项卡中添加了显示新界面元素的方法。 

  • CreatePatternSet(). 显示一组可切换形态选择按钮的新方法。
  • CreateTFLabel(). 一组时间帧的文本标题标签。
  • CreateTimeframeSet(). 一组可切换的时间帧按钮。
  • CreateDateRange(). 一个新按钮,单击该按钮可打开对话框,以便选择欲分析日期范围。
  • CreateThresholdValue(). 修正的方法,以点数为单位显示趋势阈值(图例 3)。

//+------------------------------------------------------------------+
//| “AutoSearch" 选卡                                                  |
//+------------------------------------------------------------------+
   if(!CreateTFLabel(m_text_labels[4],10,10,1))
      return(false);
//--- 按钮
   if(!CreateDualButton(m_buttons[6],m_buttons[7],200,50))
      return(false);
   if(!CreateTripleButton(m_buttons[8],m_buttons[9],m_buttons[10],10,50))
      return(false);
//--- 时间帧标题
   if(!CreateTFLabel(m_text_labels[5],10,100,1))
      return(false);
//--- 创建时间帧集合的按钮
   if(!CreateTimeframeSet(m_timeframes1,10,125,1))
      return(false);
//--- 编辑字段
   if(!CreateSymbolsFilter(m_symb_filter2,m_request2,10,180,1))
      return(false);
//--- 创建日期范围选择按钮
   if(!CreateDateRange(m_request4,280,180,1))
      return(false);
//--- 为盈利阈值创建输入字段
   if(!CreateThresholdValue(m_threshold2,400,180,100,1))
      return(false);
//--- 创建品种表格
   if(!CreateSymbTable(m_symb_table2,10,225,1))
      return(false);
//--- 创建结果表格
   if(!CreateTable2(m_table2,120,225,1))
      return(false);

此外,以下方法已从 “Setting” 选项卡移到第二个 “AutoSearch” 选项卡(图例 5):CreateTripleButton() 方法负责显示所生成形态大小选择相关的元素,以及 CreateDualButton()方法显示可切换 "Repeat/No" 选项。 添加了新方法:负责时间帧标题及其选择的方法

//+------------------------------------------------------------------+
//| ”Settings“ 选卡                                                    |
//+------------------------------------------------------------------+
//--- 创建烛条设置
   if(!CreateCandle(m_pictures[0],m_buttons[0],m_candle_names[0],"Long",10,10,"Images\\EasyAndFastGUI\\Candles\\long.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[1],m_buttons[1],m_candle_names[1],"Short",104,10,"Images\\EasyAndFastGUI\\Candles\\short.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[2],m_buttons[2],m_candle_names[2],"Spinning top",198,10,"Images\\EasyAndFastGUI\\Candles\\spin.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[3],m_buttons[3],m_candle_names[3],"Doji",292,10,"Images\\EasyAndFastGUI\\Candles\\doji.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[4],m_buttons[4],m_candle_names[4],"Marubozu",386,10,"Images\\EasyAndFastGUI\\Candles\\maribozu.bmp"))
      return(false);
   if(!CreateCandle(m_pictures[5],m_buttons[5],m_candle_names[5],"Hammer",480,10,"Images\\EasyAndFastGUI\\Candles\\hammer.bmp"))
      return(false);
//--- 文本标签
   if(!CreateTextLabel(m_text_labels[0],10,140))
      return(false);
   if(!CreateTextLabel(m_text_labels[3],300,140))
      return(false);
//--- 编辑字段
   if(!CreateCoef(m_coef1,10,180,"K1",1))
      return(false);
   if(!CreateCoef(m_coef2,100,180,"K2",0.5))
      return(false);
   if(!CreateCoef(m_coef3,200,180,"K3",0.25))
      return(false);
   if(!CreateLanguageSetting(m_lang_setting,10,240,2))
      return(false);
//--- 列表视图
   if(!CreateListView(300,180))
      return(false);
//---
   if(!CreateCheckBox(m_checkbox1,300+8,160,"All candlesticks"))
      return(false);
//--- 状态栏
   if(!CreateStatusBar(1,26))
      return(false);

”Setting“ 部分现在包含更少的元素。 它包含独立烛条设置用于计算概率和效率系数的比率设置界面语言选择,和用于在 ”AutoSearch“ 选项卡中生成形态的烛条选择。接下来,更详尽地考察新方法。 

创建形态选择的方法 CreatePatternSet()。这是一组可切换按钮,用于选择已有形态来进行分析。

图例 6 分析形态选择器的原理

实现如下:

//+------------------------------------------------------------------+
//| 创建一组形态按钮                                                       |
//+------------------------------------------------------------------+
bool CProgram::CreatePatternSet(CButton &button[],int x_gap,int y_gap)
  {
   ArrayResize(button,15);
   string pattern_names[15]=
     {
      "Hummer",
      "Invert Hummer",
      "Handing Man",
      "Shooting Star",
      "Engulfing Bull",
      "Engulfing Bear",
      "Harami Cross Bull",
      "Harami Cross Bear",
      "Harami Bull",
      "Harami Bear",
      "Doji Star Bull",
      "Doji Star Bear",
      "Piercing Line",
      "Dark Cloud Cover",
      "All Patterns"
     };
   int k1=x_gap,k2=x_gap,k3=x_gap;
   for(int i=0;i<=14;i++)
     {
      if(i<5)
        {
         CreatePatternButton(button[i],pattern_names[i],k1,y_gap);
         k1+=150;
        }
      else if(i>=5 && i<10)
        {
         CreatePatternButton(button[i],pattern_names[i],k2,y_gap+30);
         k2+=150;
        }
      else if(i>=10 && i<14)
        {
         CreatePatternButton(button[i],pattern_names[i],k3,y_gap+60);
         k3+=150;
        }
      else if(i==14)
        {
         CreatePatternButton(button[i],pattern_names[i],k3,y_gap+60);
        }
     }
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建用于选择分析形态的按钮                                                |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Candles\\passive.bmp"
#resource "\\Images\\EasyAndFastGUI\\Candles\\pressed.bmp"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreatePatternButton(CButton &button,const string candlename,const int x_gap,const int y_gap)
  {
//--- 保存指向主控件的指针
   button.MainPointer(m_tabs1);
//--- 加载到选卡
   m_tabs1.AddToElementsArray(0,button);
//--- 属性
   button.XSize(120);
   button.YSize(20);
   button.Font("Trebuchet");
   button.FontSize(9);
   button.LabelColor(clrWhite);
   button.LabelColorHover(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.IsCenterText(true);
   button.TwoState(true);
   button.IconFile("Images\\EasyAndFastGUI\\Candles\\passive.bmp");
   button.IconFilePressed("Images\\EasyAndFastGUI\\Candles\\pressed.bmp");
//--- 创建控件
   if(!button.CreateButton(candlename,x_gap,y_gap))
      return(false);
//--- 将元素指针添加到数据库
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

注意选择/取消选择所有形态的最后一个按钮 'All Patterns'。 按钮被按下之后由按钮事件响应部分中的附属代码处理:

   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- 选择并取消选择所有形态按钮
      if(lparam==m_patterns[14].Id())
        {
         if(m_patterns[14].IsPressed())
           {
            for(int i=0;i<14;i++)
               m_patterns[i].IsPressed(true);
           }
         else if(!m_patterns[14].IsPressed())
           {
            for(int i=0;i<14;i++)
               m_patterns[i].IsPressed(false);
           }
         for(int i=0;i<14;i++)
            m_patterns[i].Update(true);
        }
...
}

当前时间帧选择方法 CreateTimeframeSet() 与前一个非常相似。 它还有一组可切换的按钮,用于选择分析时间帧。

图例 7 分析时间帧选择器的原理

实现代码如下所示:

//+------------------------------------------------------------------+
//| 创建一组时间帧按钮                                                      |
//+------------------------------------------------------------------+
bool CProgram::CreateTimeframeSet(CButton &button[],int x_gap,int y_gap,const int tab)
  {
   ArrayResize(button,22);
   string timeframe_names[22]=
     {"M1","M2","M3","M4","M5","M6","M10","M12","M15","M20","M30","H1","H2","H3","H4","H6","H8","H12","D1","W1","MN","ALL"};
   int k1=x_gap,k2=x_gap;
   for(int i=0;i<22;i++)
     {
      CreateTimeframeButton(button[i],timeframe_names[i],k1,y_gap,tab);
      k1+=33;
     }
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateTimeframeButton(CButton &button,const string candlename,const int x_gap,const int y_gap,const int tab)
  {
//--- 保存指向主控件的指针
   button.MainPointer(m_tabs1);
//--- 加载到选卡
   m_tabs1.AddToElementsArray(tab,button);
//--- 属性
   button.XSize(30);
   button.YSize(30);
   button.Font("Trebuchet");
   button.FontSize(10);
   button.LabelColor(clrWhite);
   button.LabelColorHover(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.BackColor(C'200,200,200');
   button.BackColorHover(C'200,200,200');
   button.BackColorPressed(C'50,180,75');
   button.BorderColor(C'200,200,200');
   button.BorderColorHover(C'200,200,200');
   button.BorderColorPressed(C'50,180,75');
   button.IsCenterText(true);
   button.TwoState(true);
//--- 创建控件
   if(!button.CreateButton(candlename,x_gap,y_gap))
      return(false);
//--- 将元素指针添加到数据库
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

它还有一个用于选择/取消选择所有时间帧的按钮,并在按钮响应部分处理:

//--- 选择并取消选择所有形态按钮
      if(lparam==m_timeframes[21].Id())
        {
         if(m_timeframes[21].IsPressed())
           {
            for(int i=0;i<21;i++)
               m_timeframes[i].IsPressed(true);
           }
         else if(!m_timeframes[21].IsPressed())
           {
            for(int i=0;i<21;i++)
               m_timeframes[i].IsPressed(false);
           }
         for(int i=0;i<21;i++)
            m_timeframes[i].Update(true);
        }

下一个新元素是 “Date Range” 按钮。 它是新的复合样本范围设置工具的一部分。 它是通过 CreateDateRange() 方法实现的。

图例 8 分析日期范围选择器的原理

实现如下:

//+------------------------------------------------------------------+
//| 创建一个显示日期范围选择窗口的按钮                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateDateRange(CButton &button,const int x_gap,const int y_gap,const int tab)
  {
//--- 保存指向主控件的指针
   button.MainPointer(m_tabs1);
//--- 加载到选卡
   m_tabs1.AddToElementsArray(tab,button);
//--- 属性
   button.XSize(100);
   button.YSize(25);
   button.Font("Trebuchet");
   button.FontSize(10);
   button.IsHighlighted(false);
   button.IsCenterText(true);
   button.BorderColor(C'0,100,255');
   button.BackColor(clrAliceBlue);
//--- 创建控件
   if(!button.CreateButton("",x_gap,y_gap))
      return(false);
//--- 将元素指针添加到数据库
   CWndContainer::AddToElementsArray(0,button);
   return(true);
  }

按钮按下事件响应程序还包括负责显示日期范围对话框的代码:

      //---
      if(lparam==m_request3.Id())
        {
         int x=m_request3.X();
         int y=m_request3.Y()+m_request3.YSize();
         m_window[2].X(x);
         m_window[2].Y(y);
         m_window[2].OpenWindow();
         val=(m_lang_index==0)?"Настройки диапазона дат":"Date Range Settings";
         m_window[2].LabelText(val);
        }

无需描述选项卡中添加的新元素,因为它们与 "Analyze" 选项卡元素的实现类似,只有坐标参数除外。 因此,我们考察负责显示其他新窗口的方法。 

应用程序窗口的结构。 用于创建应用程序对话框的方法。

显示 ”Analyze“ 和 "AutoSearch" 选项卡对话框的方法类似,因此我们只考察其中之一。

//+------------------------------------------------------------------+
//| 在 “Analyze” 选项卡中创建日期范围选择对话框                                |
//+------------------------------------------------------------------+
bool CProgram::CreateWindowSetting2(const string caption_text)
  {
//--- 将指针添加到窗口数组
   CWndContainer::AddWindow(m_window[2]);
//--- 坐标
   int x=m_request3.X();
   int y=m_request3.Y()+m_request3.YSize();
//--- 属性
   m_window[2].XSize(372);
   m_window[2].YSize(300);
   m_window[2].WindowType(W_DIALOG);

//--- 创建窗体
   if(!m_window[2].CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   if(!CreateCalendar(m_calendar1,m_window[2],10,25,D'01.01.2018',2))
      return(false);
   if(!CreateCalendar(m_calendar2,m_window[2],201,25,m_calendar2.Today(),2))
      return(false);
//---
   if(!CreateTimeEdit(m_time_edit1,m_window[2],10,200,"Time",2))
      return(false);
   if(!CreateTimeEdit(m_time_edit2,m_window[2],200,200,"Time",2))
      return(false);
//---
   return(true);
  }


计算部分。 重新设计的烛条和形态搜索器方法。

由于用户界面结构的重大变化,且由于添加了新元素,并删除了一些旧元素,计算方法也发生了变化。 当前应用程序中有两种计算方法:第一种用于已有形态,第二种方法用于生成的形态。 

点击品种列表中的一个可用金融交易工具后会启动计算。 此规则适用于两个选项卡,”Analyze“ 和 ”AutoSearch“。 根据选项卡调用两种方法之一。

//+------------------------------------------------------------------+
//| 在 Analyze 选卡里品种变化                                              |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol1(const long id)
  {
//--- 检查元素 ID
   if(id!=m_symb_table1.Id())
      return(false);
//--- 如果未选择该行,则退出
   if(m_symb_table1.SelectedItem()==WRONG_VALUE)
     {
      //--- 在状态栏中显示完整的品种说明
      m_status_bar.SetValue(0,"No symbol selected for analysis");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- 获取品种
   string symbol=m_symb_table1.GetValue(0,m_symb_table1.SelectedItem());
//--- 在状态栏中显示完整的品种说明
   string val=(m_lang_index==0)?"Выбранный символ: ":"Selected symbol: ";
   m_status_bar.SetValue(0,val+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(0).Update(true);
//---
   GetPatternType(symbol);
   return(true);
  }
//+------------------------------------------------------------------+
//| 在 AutoSearch 选卡里品种变化                                           |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol2(const long id)
  {
//--- 检查元素 ID
   if(id!=m_symb_table2.Id())
      return(false);
//--- 如果未选择该行,则退出
   if(m_symb_table2.SelectedItem()==WRONG_VALUE)
     {
      //--- 在状态栏中显示完整的品种说明
      m_status_bar.SetValue(0,"No symbol selected for analysis");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- 获取品种
   string symbol=m_symb_table2.GetValue(0,m_symb_table2.SelectedItem());
//--- 在状态栏中显示完整的品种说明
   string val=(m_lang_index==0)?"Выбранный символ: ":"Selected symbol: ";
   m_status_bar.SetValue(0,val+::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(0).Update(true);
//---
   if(!GetCandleCombitation())
     {
      if(m_lang_index==0)
         MessageBox("Число выбранных свечей меньше размера исследуемого паттерна!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("The number of selected candles is less than the size of the studied pattern!","Error",MB_OK);
      return(false);
     }
//---
   GetPatternType(symbol,m_total_combination);
   return(true);
  }

在每个方法的末尾调用含有两种不同参数类型的 GetPattertType() 方法。 这是搜索形态和处理所得结果的关键方法。 现在我们来详细考察每个方法。

第一个方法类型用于搜索已有形态。

   bool              GetPatternType(const string symbol);

该方法的实现相当长,因此这里将仅提供一个形态的示例。

//+------------------------------------------------------------------+
//| 形态识别                                                           |
//+------------------------------------------------------------------+
bool CProgram::GetPatternType(const string symbol)
  {
   CANDLE_STRUCTURE cand1,cand2;
//---
   RATING_SET hummer_coef[];
   RATING_SET invert_hummer_coef[];
   RATING_SET handing_man_coef[];
   RATING_SET shooting_star_coef[];
   RATING_SET engulfing_bull_coef[];
   RATING_SET engulfing_bear_coef[];
   RATING_SET harami_cross_bull_coef[];
   RATING_SET harami_cross_bear_coef[];
   RATING_SET harami_bull_coef[];
   RATING_SET harami_bear_coef[];
   RATING_SET doji_star_bull_coef[];
   RATING_SET doji_star_bear_coef[];
   RATING_SET piercing_line_coef[];
   RATING_SET dark_cloud_cover_coef[];
//--- 接收所选时间帧的数据
   GetTimeframes(m_timeframes,m_cur_timeframes1);
   int total=ArraySize(m_cur_timeframes1);
//--- 检查至少一个选定的时间帧
   if(total<1)
     {
      if(m_lang_index==0)
         MessageBox("Вы не выбрали рабочий таймфрейм!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("You have not selected a working timeframe!","Error",MB_OK);
      return(false);
     }
   int count=0;
   m_total_row=0;
   m_table_number=1;
//--- 删除所有行
   m_table1.DeleteAllRows();
//--- 获取日期范围
   datetime start=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit1.GetHours()+":"+(string)m_time_edit1.GetMinutes()+":00");
   datetime end=StringToTime(TimeToString(m_calendar2.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit2.GetHours()+":"+(string)m_time_edit2.GetMinutes()+":00");
//--- 检查指定的日期
   if(start>end || end>TimeCurrent())
     {
      if(m_lang_index==0)
         MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("Incorrect date range selected!","Error",MB_OK);
      return(false);
     }
//--- 锤子,多头形态
   if(m_patterns[0].IsPressed())
     {
      ArrayResize(m_hummer_total,total);
      ArrayResize(hummer_coef,total);
      ZeroMemory(m_hummer_total);
      ZeroMemory(hummer_coef);
      ZeroMemory(cand1);
      count++;
      //--- 按时间帧计算
      for(int j=0;j<total;j++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         int copied=CopyRates(symbol,m_cur_timeframes1[j],start,end,rt);
         for(int i=0;i<copied;i++)
           {
            GetCandleType(symbol,cand1,m_cur_timeframes1[j],i);             // 当前烛条
            if(cand1.trend==DOWN &&                                        // 检查趋势方向
               cand1.type==CAND_HAMMER)                                    // 检查 "锤子"
              {
               m_hummer_total[j]++;
               GetCategory(symbol,i+3,hummer_coef[j],m_cur_timeframes1[j],m_threshold_value1);
              }
           }
         AddRow(m_table1,"Hammer",hummer_coef[j],m_hummer_total[j],m_cur_timeframes1[j]);
        }
     }
...
//---
   if(count>0)
     {
      //---
      m_table1.DeleteRow(m_total_row);
      //--- 刷新表格
      m_table1.Update(true);
      m_table1.GetScrollVPointer().Update(true);
     }
   else
     {
      if(m_lang_index==0)
         MessageBox("Вы не выбрали паттерн!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("You have not chosen a pattern!","Error",MB_OK);
     }
   return(true);
  }

操作算法执行如下:

  • 更新计算所需的结构。
  • 使用 GetTimeframes() 方法从 UI 数据中获取有关所选时间帧的信息。
  • 未选择时间帧时处理可能的错误。
  • 如果至少选择了一个时间帧,则获取日期范围的数据并检查输入是否正确。 此外,应该没有输入错误。
  • 然后检查是否选择了每种可能的形态进行计算,并在之前选择的时间帧和范围内搜索该形态。
  • 最后检查是否有至少一个选定的形态能用来计算。 如果选择了形态,则输出到结果表格。 如果未选择任何形态,则输出相应的消息。

接下来,我们考察早期版本中存在但在新版本中进行了修改的方法,以及新的计算所获数据并将其输出到结果表格的方法:

  • 指定烛条类型搜索方法 GetCandleType()
  • 找到形态效率评估方法 GetCategory()
  • 计算所获形态数据并将结果输出到表格的方法 AddRow()

形态搜索的方法 GetPatternType() 有两种不同的实现,而这三种方法是通用的。 我们详细考察一下:

//+------------------------------------------------------------------+
//| 烛条类型识别                                                         |
//+------------------------------------------------------------------+
bool CProgram::GetCandleType(const string symbol,CANDLE_STRUCTURE &res,ENUM_TIMEFRAMES timeframe,const int shift)
  {
   MqlRates rt[];
   int aver_period=5;
   double aver=0.0;
   datetime start=TimeCurrent();
   SymbolSelect(symbol,true);
   //--- 从范围中获取开始日期,具体取决于形态的类型
   if(m_table_number==1)
      start=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit1.GetHours()+":"+(string)m_time_edit1.GetMinutes()+":00");
   else if(m_table_number==2)
      start=StringToTime(TimeToString(m_calendar3.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit3.GetHours()+":"+(string)m_time_edit3.GetMinutes()+":00");
//--- 偏移日期 
   start+=PeriodSeconds(timeframe)*shift;
   int copied=CopyRates(symbol,timeframe,start,aver_period+1,rt);
   if(copied<6)
     {
      Print(start,": Not enough data for calculation — ",GetLastError());
     }
//--- 获取前一根烛条的详细信息
   if(copied<aver_period)
      return(false);
//---
   res.open=rt[aver_period].open;
   res.high=rt[aver_period].high;
   res.low=rt[aver_period].low;
   res.close=rt[aver_period].close;
//--- 判断趋势方向
   for(int i=0;i<aver_period;i++)
      aver+=rt[i].close;

   aver/=aver_period;

   if(aver<res.close)
      res.trend=UPPER;
   if(aver>res.close)
      res.trend=DOWN;
   if(aver==res.close)
      res.trend=FLAT;
//--- 判断它是多头还是空头烛条
   res.bull=res.open<res.close;
//--- 获得烛条实体的绝对大小
   res.bodysize=MathAbs(res.open-res.close);
//--- 获取阴影的大小
   double shade_low=res.close-res.low;
   double shade_high=res.high-res.open;
   if(res.bull)
     {
      shade_low=res.open-res.low;
      shade_high=res.high-res.close;
     }
   double HL=res.high-res.low;
//--- 计算之前烛条的平均实体高度
   double sum=0;
   for(int i=1; i<=aver_period; i++)
      sum+=MathAbs(rt[i].open-rt[i].close);
   sum/=aver_period;

//--- 判断烛条类型   
   res.type=CAND_NONE;
//--- 长实体 
   if(res.bodysize>sum*m_long_coef && res.bull)
      res.type=CAND_LONG_BULL;
//--- 短实体 
   if(res.bodysize<sum*m_short_coef && res.bull)
      res.type=CAND_SHORT_BULL;
//--- 长实体空头
   if(res.bodysize>sum*m_long_coef && !res.bull)
      res.type=CAND_LONG_BEAR;
//--- 短实体空头
   if(res.bodysize<sum*m_short_coef && !res.bull)
      res.type=CAND_SHORT_BEAR;
//--- 十字星
   if(res.bodysize<HL*m_doji_coef)
      res.type=CAND_DOJI;
//--- marubozu
   if((shade_low<res.bodysize*m_maribozu_coef && shade_high<res.bodysize*m_maribozu_coef) && res.bodysize>0)
      res.type=CAND_MARIBOZU;
//--- 锤子
   if(shade_low>res.bodysize*m_hummer_coef2 && shade_high<res.bodysize*m_hummer_coef1)
      res.type=CAND_HAMMER;
//--- 逆锤子
   if(shade_low<res.bodysize*m_hummer_coef1 && shade_high>res.bodysize*m_hummer_coef2)
      res.type=CAND_INVERT_HAMMER;
//--- 顶部纺锤
   if((res.type==CAND_SHORT_BULL || res.type==CAND_SHORT_BEAR) && shade_low>res.bodysize*m_spin_coef && shade_high>res.bodysize*m_spin_coef)
      res.type=CAND_SPIN_TOP;
//---
   ArrayFree(rt);
   return(true);
  }

方法算法如下:根据您要分析的形态,已有或生成的形态,从选择范围获取初始日期。 由于此方法用于在日期范围内的计算循环,因此将给定时间帧内一根烛条的初始日期进行修改,从过去向未来偏移。 然后复制计算简单烛条类型所需的数据。 如果数据不足,则向用户提示相应的消息。 

重要提示! 有必要监控 MetaTrader 5 终端中历史数据的可用性,否则应用程序可能无法正常工作。 

如果数据足够,则检查当前烛条是否属于简单的烛条类型。

正如先前文章所知,GetCategory() 方法在形态发生后会依据历史数据检查价格行为。

//+------------------------------------------------------------------+
//| 判定利润类别                                                         |
//+------------------------------------------------------------------+
bool CProgram::GetCategory(const string symbol,const int shift,RATING_SET &rate,ENUM_TIMEFRAMES timeframe,int threshold)
  {
   MqlRates rt[];
   datetime start=TimeCurrent();
   if(m_table_number==1)
      start=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit1.GetHours()+":"+(string)m_time_edit1.GetMinutes()+":00");
   else if(m_table_number==2)
      start=StringToTime(TimeToString(m_calendar3.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit3.GetHours()+":"+(string)m_time_edit3.GetMinutes()+":00");
   start+=PeriodSeconds(timeframe)*shift;
   int copied=CopyRates(symbol,timeframe,start,4,rt);
//--- 获取前一根烛条的详细信息
   if(copied<4)
     {
      return(false);
     }
   double high1,high2,high3,low1,low2,low3,close0,point;
   close0=rt[0].close;
   high1=rt[1].high;
   high2=rt[2].high;
   high3=rt[3].high;
   low1=rt[1].low;
   low2=rt[2].low;
   low3=rt[3].low;
   if(!SymbolInfoDouble(symbol,SYMBOL_POINT,point))
      return(false);

//--- 检查它是否为上行趋势
   if((int)((high1-close0)/point)>=threshold)
     {
      rate.a_uptrend++;
     }
   else if((int)((high2-close0)/point)>=threshold)
     {
      rate.b_uptrend++;
     }
   else if((int)((high3-close0)/point)>=threshold)
     {
      rate.c_uptrend++;
     }

//--- 检查它是否为下行趋势
   if((int)((close0-low1)/point)>=threshold)
     {
      rate.a_dntrend++;
     }
   else if((int)((close0-low2)/point)>=threshold)
     {
      rate.b_dntrend++;
     }
   else if((int)((close0-low3)/point)>=threshold)
     {
      rate.c_dntrend++;
     }
   return(true);
  }

在该方法的算法中,仅获取有关所分析烛条数据的方法有所改变。 这与新的时间范围选择工具直接相关。

GetPatternType() 的最后一个常用方法是在结果表格中获取、计算和显示数据。 

//+------------------------------------------------------------------+
//| 获取,计算和显示结果表格中的数据                                           |
//+------------------------------------------------------------------+
void CProgram::AddRow(CTable &table,string pattern_name,RATING_SET &rate,int found,ENUM_TIMEFRAMES timeframe)
  {
   int row=m_total_row;
   int total_patterns=ArraySize(m_total_combination);
   double p1,p2,k1,k2;
   int sum1=0,sum2=0;
   sum1=rate.a_uptrend+rate.b_uptrend+rate.c_uptrend;
   sum2=rate.a_dntrend+rate.b_dntrend+rate.c_dntrend;
//---
   p1=(found>0)?NormalizeDouble((double)sum1/found*100,2):0;
   p2=(found>0)?NormalizeDouble((double)sum2/found*100,2):0;
   k1=(found>0)?NormalizeDouble((m_k1*rate.a_uptrend+m_k2*rate.b_uptrend+m_k3*rate.c_uptrend)/found,3):0;
   k2=(found>0)?NormalizeDouble((m_k1*rate.a_dntrend+m_k2*rate.b_dntrend+m_k3*rate.c_dntrend)/found,3):0;

//---
   table.AddRow(row);
   if(m_table_number==1)
      table.SetValue(0,row,pattern_name);
   else if(m_table_number==2)
     {
      if(row<total_patterns)
         table.SetValue(0,row,m_total_combination[row]);
      else if(row>=total_patterns)
        {
         int i=row-int(total_patterns*MathFloor(double(row)/total_patterns));
         table.SetValue(0,row,m_total_combination[i]);
        }
     }
   table.SetValue(1,row,(string)found);
   table.SetValue(2,row,TimeframeToString(timeframe));
   table.SetValue(3,row,(string)p1,2);
   table.SetValue(4,row,(string)p2,2);
   table.SetValue(5,row,(string)k1,2);
   table.SetValue(6,row,(string)k2,2);
   ZeroMemory(rate);
   m_total_row++;
  }
//+------------------------------------------------------------------+

该方法在其参数接收所有计算所需数据。 数据处理算法非常简单。 这里应该提到两个要点。 在之前的应用程序版本中,事先知道确切的行数。 例如,当处理已有的形态数据时,结果表格总是拥有与预设形态的数量相同的行数,即 14。 现在,用户可以选择任意数量的形态或操作时间帧,因此行数是未知的。 所以添加了一个简单的行计数器 m_total_row。 调用 AddRow() 方法会根据两个符号向结果表格里添加一行:pattern 和 timeframe。

第二点涉及 ”AutoSearch“ 选项卡。 在以前的版本中,有限行数等于生成形态的组合数。 出于同样的原因,以前的算法不适合当前状况了:时间帧的数量是未知的。 因此,应为每个选定的时间帧再次写入已生成组合的整个数组。

我们来考察 GetPatternType() 方法的第二个变体。

bool              GetPatternType(const string symbol,string &total_combination[]);

此处,除了当前品种之外,第二个参数是已生成形态的字符串数组的链接。 

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::GetPatternType(const string symbol,string &total_combination[])
  {
   CANDLE_STRUCTURE cand1[],cand2[],cand3[],cur_cand,prev_cand,prev_cand2;
   RATING_SET ratings;
   int total_patterns,m_pattern_total[];
   string elements[];
//---
   total_patterns=ArraySize(total_combination);
   ArrayResize(cand1,total_patterns);
   ArrayResize(cand2,total_patterns);
   ArrayResize(cand3,total_patterns);
   ArrayResize(m_pattern_total,total_patterns);
   ArrayResize(elements,m_pattern_size);
//---
   for(int i=0;i<total_patterns;i++)
     {
      StringReplace(total_combination[i],"[","");
      StringReplace(total_combination[i],"]","");
      if(m_pattern_size>1)
        {
         ushort sep=StringGetCharacter(",",0);
         StringSplit(total_combination[i],sep,elements);
        }
      m_pattern_total[i]=0;
      if(m_pattern_size==1)
         IndexToPatternType(cand1[i],(int)total_combination[i]);
      else if(m_pattern_size==2)
        {
         IndexToPatternType(cand1[i],(int)elements[0]);
         IndexToPatternType(cand2[i],(int)elements[1]);
        }
      else if(m_pattern_size==3)
        {
         IndexToPatternType(cand1[i],(int)elements[0]);
         IndexToPatternType(cand2[i],(int)elements[1]);
         IndexToPatternType(cand3[i],(int)elements[2]);
        }
     }
//---
   GetTimeframes(m_timeframes1,m_cur_timeframes2);
   int total=ArraySize(m_cur_timeframes2);
   if(total<1)
     {
      if(m_lang_index==0)
         MessageBox("Вы не выбрали рабочий таймфрейм!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("You have not selected a working timeframe!","Error",MB_OK);
      return(false);
     }
   m_total_row=0;
   m_table_number=2;
//--- 删除所有行
   m_table2.DeleteAllRows();
//---
   datetime start=StringToTime(TimeToString(m_calendar3.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit3.GetHours()+":"+(string)m_time_edit3.GetMinutes()+":00");
   datetime end=StringToTime(TimeToString(m_calendar4.SelectedDate(),TIME_DATE)+" "+(string)m_time_edit4.GetHours()+":"+(string)m_time_edit4.GetMinutes()+":00");
//---
   if(start>end || end>TimeCurrent())
     {
      if(m_lang_index==0)
         MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK);
      else if(m_lang_index==1)
         MessageBox("Incorrect date range selected!","Error",MB_OK);
      return(false);
     }
//---
   if(m_pattern_size==1)
     {
      ZeroMemory(cur_cand);
      //--- 按时间帧计算
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- 按形态计算
         for(int j=0;j<total_patterns;j++)
           {
            //--- 按日期范围计算         
            for(int k=0;k<copied;k++)
              {
               //--- 计算当前烛条类型
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                 // current candlestick
               //---
               if(cur_cand.type==cand1[j].type && cur_cand.bull==cand1[j].bull)
                 {
                  m_pattern_total[j]++;
                  GetCategory(symbol,k+3,ratings,m_cur_timeframes2[i],m_threshold_value2);
                 }
              }
            AddRow(m_table2,"",ratings,m_pattern_total[j],m_cur_timeframes2[i]);
            m_pattern_total[j]=0;
           }
        }
     }
   else if(m_pattern_size==2)
     {
      ZeroMemory(cur_cand);
      ZeroMemory(prev_cand);
      //--- 按时间帧计算
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(rt);
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- 按形态计算
         for(int j=0;j<total_patterns;j++)
           {
            //--- 按日期范围计算         
            for(int k=0;k<copied;k++)
              {
               //--- 计算当前烛条类型
               GetCandleType(symbol,prev_cand,m_cur_timeframes2[i],k+1);               // previous candlestick
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                  // current candlestick
               //---
               if(cur_cand.type==cand1[j].type && cur_cand.bull==cand1[j].bull && 
                  prev_cand.type==cand2[j].type && prev_cand.bull==cand2[j].bull)
                 {
                  m_pattern_total[j]++;
                  GetCategory(symbol,k+4,ratings,m_cur_timeframes2[i],m_threshold_value2);
                 }
              }
            AddRow(m_table2,"",ratings,m_pattern_total[j],m_cur_timeframes2[i]);
            m_pattern_total[j]=0;
           }
        }
     }
   else if(m_pattern_size==3)
     {
      ZeroMemory(cur_cand);
      ZeroMemory(prev_cand);
      ZeroMemory(prev_cand2);
      //--- 按时间帧计算
      for(int i=0;i<total;i++)
        {
         MqlRates rt[];
         ZeroMemory(ratings);
         int copied=CopyRates(symbol,m_cur_timeframes2[i],start,end,rt);
         //--- 按形态计算
         for(int j=0;j<total_patterns;j++)
           {
            //--- 按日期范围计算         
            for(int k=0;k<copied;k++)
              {
               //--- 计算当前烛条类型
               GetCandleType(symbol,prev_cand2,m_cur_timeframes2[i],k+2);                                  // 之前的烛条
               GetCandleType(symbol,prev_cand,m_cur_timeframes2[i],k+1);                                   // 之前的烛条
               GetCandleType(symbol,cur_cand,m_cur_timeframes2[i],k);                                      // 当前的烛条
               //---
               if(cur_cand.type==cand1[j].type && cur_cand.bull==cand1[j].bull && 
                  prev_cand.type==cand2[j].type && prev_cand.bull==cand2[j].bull && 
                  prev_cand2.type==cand3[j].type && prev_cand2.bull==cand3[j].bull)
                 {
                  m_pattern_total[j]++;
                  GetCategory(symbol,k+5,ratings,m_cur_timeframes2[i],m_threshold_value2);
                 }
              }

            AddRow(m_table2,"",ratings,m_pattern_total[j],m_cur_timeframes2[i]);
            m_pattern_total[j]=0;
           }
        }
     }
//---
   m_table2.DeleteRow(m_total_row);
//--- 刷新表格
   m_table2.Update(true);
   m_table2.GetScrollVPointer().Update(true);
   return(true);
  }

重要的是理解此版本算法中根据输入数据的计算顺序。 我们不会在此考察如何接收时间帧和日期范围,因为这已在前面论述过。 然后算法检查当前正在测试的形态的大小。 考虑一个三烛条形态。 在声明存储价格数据的结构,且将 'ratings' 在结构清空之后,该算法首次循环遍历时间帧并获取每个时间帧复制数据的数量。 这能够判定进一步搜索指定形态的范围。 在时间帧循环之后,在指定的时间帧内为每个形态进行计算循环。 然后,对于每个形态,在指定日期范围内循环遍历所定义的烛条。

为了更好地理解,查看结果表格中的计算示例和信息显示顺序。

图例 9. 表格中的计算示例和结果输出顺序

从图例 9 中可以看出,测试是在 M15,M30,H1 和 H2 时间帧上使用 EURUSD 货币对,和 1 根烛条形态进行的。 选择了两个简单的烛条进行测试:索引为 1 和 2。 可以在结果表格中观察到上述算法的实现。 它如下执行:首先在 15 分钟的时间帧内逐个分析所有生成的形态,然后在 30 分钟的时间帧内分析,依此类推。

结束语

下面附带的存档所含所有上述文件,并按相应文件夹中的位置排列。 若要正常操作,您只需将 MQL5 文件夹保存到终端文件夹中。 若要打开 MQL5 文件夹所在的终端根目录,请在 MetaTrader 5 终端中按 Ctrl+Shift+D 组合键,或使用关联菜单,如下面的图例 10 中所示。

图例 10 在 MetaTrader 5 终端根目录中打开 MQL5 文件夹。

本文中用到的程序

#
 名称
类型
说明
1
PatternAnalyzer.mq5 图形界面
 分析烛条形态的工具栏
2 MainWindow.mqh 代码库  GUI 函数库
3 Program.mqh 代码库  用于创建 UI 和计算元素的方法库

本系列的前几篇文章:

研究烛条分析技术(第一部分):检查现有形态
研究烛条分析技术(第二部分):自动搜索新形态
研究烛条分析技术(第三部分):操作形态的函数库

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

附加的文件 |
MQL5.zip (495.94 KB)
最近评论 | 前往讨论 (2)
Zqh Zhang
Zqh Zhang | 8 12月 2020 在 07:21
调试闪退的建议注销这两项, 自己加个 时间框,,不清楚为什么  这两个时间框会引起闪退, 只 注释掉的命令
Zqh Zhang
Zqh Zhang | 8 12月 2020 在 07:26
非常感谢您的文章, 写得很棒, 解决了 我一直都在思考而有不知道 怎么解决的问题,,

 Большое спасибо 

如何基于HTML和CSV报表可视化多币种交易历史 如何基于HTML和CSV报表可视化多币种交易历史
自推出以来,MetaTrader 5提供了多货币测试选项,也许交易者经常使用这个功能。然而,这种功能并不是万能的。本文介绍了几种基于HTML和CSV交易历史报告的图表图形绘制程序,多货币交易可以在多个子窗口以及使用动态切换命令的一个窗口中并行分析。
开发一个跨平台网格 EA 开发一个跨平台网格 EA
在本文中,我们将学习如何创建在 MetaTrader 4 和 MetaTrader 5 中都能工作的 EA 交易。为此,我们将开发一个 EA 构建的订单网格,网格是指将多个限价订单置于当前价格之上,同时将相同数量的限价订单置于当前价格之下的 EA 交易。
轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件 轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件
在之前的文章中,我们已着手创建一个大型跨平台函数库,简化 MetaTrader 5 和 MetaTrader 4 平台程序的开发。 我们已拥有历史订单和成交集合,在场订单和仓位的集合,以及便捷选择和订单排序的类。 在这一部分中,我们将继续开发基础对象,并教导引擎(Engine)函数库跟踪帐户上的交易事件。
10 分钟掌握 MQL5 的 DLL(第二部分):使用 Visual Studio 2017 创建 10 分钟掌握 MQL5 的 DLL(第二部分):使用 Visual Studio 2017 创建
初版文章依然具有其相关性,因此如果您对此主题感兴趣,请务必阅读第一篇文章。 从初版起已经过了很久时间,而当前的 Visual Studio 2017 具有全新的界面。 MetaTrader 5 平台也拥有了诸多新功能。 本文提供了开发 DLL 项目各个阶段的描述,以及如何设置 DLL 并与 MetaTrader 5 工具进行交互。