将入场信息解析到指标

Dmitriy Gizlyk | 18 十二月, 2017


引言

当看到成功交易者的一连串盈利交易时, 您是否希望追随他的策略?或是在查看您的交易历史时, 您也许会想到如何摆脱亏损交易?我相信, 你们当中的许多人至少会正面地回答一个问题。在这篇文章中, 我打算推荐一些将交易历史解析为指标的方法, 另外我将分享如何选择有助于提高交易性能的指标。

1. 问题定义

在我 之前的文章 中, 我曾讲述过构建基于卡尔曼滤波器的智能交易系统。在测试中, 它表现出可盈利, 但同时也暴露出两个策略瓶颈: 退出稍晚, 以及在横盘走势时的一些亏损交易。

因此, 我们的目标是减少这一策略下的亏损交易数量。为此, 在开仓之时保存数个指标的数值。然后, 分析比较指标数值与交易结果。剩下的就是在有助于改善交易性能的指标之间进行选择。

首先, 制定行动计划。

  1. 确定测试期限。测试并保存报表
  2. 解析测试报表并将一系列交易 (包括操作结果) 放进数组
  3. 确定要使用的指标列表和数据保存格式。准备进一步应用程序的类
  4. 为输出结果准备报表表格。
  5. 建立分析 EA。
  6. 开始在策略测试器中测试 EA, 并分析报表。
  7. 在 EA 中添加必要的指标。
  8. 测试更新后的 EA 并比较结果。

2. 针对所分析 EA 进行的第一次测试

在上述 文章 中, EA 在一个月内完成了 150 笔交易。这不足以进行统计分析。为了使结果具有代表性, 将测试期限提高八倍。未有任何优化, 设置自回归函数的周期为 3120 根柱线 (约 3 个月), 并开始测试。

首次测试。首次测试

从测试结果当中, 我们得到了一个明显的亏损余额图, 在 1-2 次盈利交易之后随即出现了一些亏损的交易。一般来说, 盈利交易的份额占比不到 34%。虽然, 平均利润超过平均损失 45%。在整个测试期间, 这不足以获得利润。

首次测试

价格图表显示, 如果没有明确定义的趋势 (横盘), EA 开仓之后会以亏损平仓。我们的任务是减少这种交易的数量, 并在可能的情况下将其完全排除在外。

测试图表

首先, 应保存测试报表以便进一步处理。然而, 有一点细微出入: 出于安全原因, 在 MQL5 中对文件操作实施了严格的控制。确保通过 MQL5 工具执行操作的文件位于 "沙盒" 之中。所以, 报表必须保存在其中。但是由于我们将在策略测试器中启动程序, 所以我们必须考虑到每个代理是在其 "沙盒" 内工作。所以, 在任何代理上进行测试期间, 程序都可以访问报表, 我们将把它保存在终端共享文件夹中。

要找到客户端共享文件夹的路径, 打开 MetaEditor 中的 "文件" 菜单并选择 "打开公用数据文件夹"。

指向 "沙盒" 的路径

在打开的窗口中, 进入 "Files" 文件夹。

指向 "沙盒" 的路径

然后按 "Ctrl+C" 将完整路径复制到剪裁板。

指向 "沙盒" 的路径

指向 "沙盒" 的路径已知, 现在我们可以保存我们的测试报表。为了做到这一点, 在策略测试器中选择 "结果", 并在任意空白处点击鼠标右键。在出现的菜单中, 选择 "报表" -> "HTML (Internet Explorer)"。

保存报表。

执行完这些操作后, 会打开一个用于保存文件的系统窗口。首先, 把我们的 "沙盒" 路径填入文件名输入字段, 然后按 "保存"。此操作将更改保存文件的文件夹。

保存报表

在随后的步骤中, 指定测试报表的名称并保存该文件。

保存报表

在 "沙盒" 中保存报表之后, 进入下一个工作阶段 - 为后续分析创建一个交易数组。

3. 生成交易阵列

3.1. 解析的一般概念

在前一章节中, 我们保存了 EA 的测试报表。现在我们要形成一个交易数组以便于处理。在浏览器中, 我们看到一个交易列表, 但 MQL5 程序不能直接从 html 文件中提取数据数组。所以, 应该实现报表的解析。

报表中的交易列表。

实质上, HTML 文件是一个由描述其格式和设计的标签切分的文本。在文本编辑器中打开报表后, 您可以轻松地找到 2 个 "<table>”标签, 这意味着报表中的所有数据被划分为 2 个数据表格。交易信息在第二张表格中。一开始的是订单信息然后 - 是交易信息。

HTML-报表的视图。

表格的行以 "<tr>...</tr>" 标签作为标记。在一行内, 信息以标签 "<td>...</td>" 划分为单元格。

3.2. 保存交易信息的类

我们已确定了报表中的数据显示格式。现在我们处理数组中的数据保存格式。迄今为止只针对一个品种分析了 EA 操作, 品名也许没能保存。尽管如此, 我们需要它来初始化指标。最后, 交易的记录结构将有以下的字段:

  • 开仓时间;
  • 开仓交易量;
  • 交易方向;
  • 平仓交易量;
  • 佣金数额;
  • 库存费数额;
  • 盈利数额。

我们已经确定了这个工作阶段的主要方面。我们开始编写代码。首先, 生成一个成交类 CDeal。

class CDeal       :  public CObject
  {
private:
   datetime          OpenTime;         // 开仓时间
   double            OpenedVolume;     // 开仓交易量
   ENUM_POSITION_TYPE Direct;          // 开仓方向
   double            ClosedVolume;     // 平仓交易量
   double            Comission;        // 开仓佣金
   double            Swap;             // 持仓库存费
   double            Profit;           // 仓位盈利
   
public:
                     CDeal();
                    ~CDeal();
  };

当记录到新开单交易时, 我们将初始化类, 此刻开仓时间, 交易量和方向都已明确。所以, 初始化函数的参数, 它们的数值和佣金 (如果有的话) 将会传送。其它参数在初始化时清零。作为结果, 类初始化函数如下所示:

CDeal::CDeal(ENUM_POSITION_TYPE type,datetime time,double volume,double comission=0.0)  : ClosedVolume(0),
                                                                                          Swap(0),
                                                                                          Profit(0)
  {
   OpenTime      = time;
   OpenedVolume  = volume;
   Direct        = type;
   Comission     = comission;
  }

在进一步的工作中, 我们需要检查已保存的交易状态。为此, 使用 IsClosed 函数检查一笔交易是否已经平仓。在其内将比较开仓/平仓的交易量。如果它们相等, 则意味着交易已平仓, 函数将返回 "true" 值。如果交易未平仓, 该函数将返回 "false", 剩余交易量依然持仓。

bool CDeal::IsClosed(double &opened_volume)
  {
   opened_volume=OpenedVolume-ClosedVolume;
   return (opened_volume<=0);
  }

如果我们只需要检查一个交易的状态, 且没有必要找到未平仓的交易量, 只要再编写一个同名函数。

bool CDeal::IsClosed(void)
  {
   double opened_volume;
   return IsClosed(opened_volume);
  }

为了正确地平仓, 我们应该知道它的类型。编写 "GetType" 函数, 返回 "private" 值至 "Direct" 变量。函数比较短, 因此可在类的实体内重写它。

ENUM_POSITION_TYPE Type(void) {  return Direct; }

在状态检查之后, 未平交易应被平仓。为此, 创建 "Close" 函数。以下参数将传递给它: 平仓交易量, 交易利润, 佣金和累计的库存费。如果传递的交易量超过未平仓交易量, 该函数将返回 "false"。在其它情况下, 传递的参数将被保存到相应的类变量中, 函数将返回 "true"。

bool CDeal::Close(double volume,double profit,double comission=0.0,double swap=0.0)
  {
   if((OpenedVolume-ClosedVolume)<volume)
      return false;
   ClosedVolume   += volume;
   Profit         += profit;
   Comission      += comission;
   Swap           += swap;
   return true;
  }

在进一步分析交易时, 我们将需要一个函数, 根据请求将返回交易利润。我们称此函数为 GetProfit。

double CDeal::GetProfit(void)
  {
   return (Comission+Swap+Profit);
  }

同样, 为了及时收到指标状态数据, 我们需要知道交易时间。为此目的, 创建 "GetTime" 函数。

datetime          GetTime(void)  {  return OpenTime;  }

3.3. 解析报表的类

创建一个类来存储每笔交易的信息之后, 我们立即解析报表。为此, 创建 "CParsing" 类。在类中确定:

  • 类对象 CArrayObj - 保存交易数组;
  • 类对象 CFileTxt - 操作报表文件;
  • 字符串类型变量 - 保存品名。

除了初始化和逆初始化函数外, 这个类中还有两个函数:

  • ReadFile — 立即解析;
  • GetSymbol — 根据要求返回品名。

class CParsing
  {
private:
   CArrayObj        *car_Deals;     //成交数组
   CFileTxt         *c_File;        //解析的文件
   
   string            s_Symbol;      //成交品种
   
public:
                     CParsing(CArrayObj *&array);
                    ~CParsing();
                    
   bool              ReadFile(string file_name);
   string            GetSymbol(void)   {  return s_Symbol;  }
  };

该类的函数主要目的是创建交易数组以便进一步处理。这意味着创建的数组必须可在主程序中工作。为此目的, 存储交易数组的 CArrayObj 类对象将在主程序中声明, 并且在初始化时将它的引用传递给类。作为结果, 初始化函数如下所示:

CParsing::CParsing(CArrayObj *&array)  :  s_Symbol(NULL)
  {
   if(CheckPointer(array)==POINTER_INVALID)
     {
      array=new CArrayObj();
     }
   car_Deals=array;
  }

在逆初始化函数中编写删除 CFileTxt 类对象的代码。在 CFile 父类的逆初始化函数中关闭文件的代码, 我们不会在这里提供。

CParsing::~CParsing()
  {
   if(CheckPointer(c_File)!=POINTER_INVALID)
      delete c_File;
  }

我们立即进行解析。调用 ReadFile 解析函数时在参数中指定报表文件名。我们在函数中所做的第一件事, 就是检查传入的参数是否为空。此外, 检查数组的可用性, 以便保存有关的交易信息。如果至少有一个条件不符合, 则终止函数执行并返回 "false"。

bool CParsing::ReadFile(string file_name)
  {
   //---
   if(file_name==NULL || file_name=="" || CheckPointer(car_Deals)==POINTER_INVALID)
      return false;

然后初始化 CFileTxt 类对象并尝试打开在函数参数中传递的文件。如果发生错误, 则以 "false" 结果退出函数。

   if(CheckPointer(c_File)==POINTER_INVALID)
     {
      c_File=new CFileTxt();
      if(CheckPointer(c_File)==POINTER_INVALID)
         return false;
     }
   //---
   if(c_File.Open(file_name,FILE_READ|FILE_COMMON)<=0)
      return false;

打开文件后, 将其全部内容读入 "字符串" 类型变量。如果文件内容为空, 以 "false" 结果退出函数。

   string html_report=NULL;
   while(!c_File.IsEnding())
      html_report+=c_File.ReadString();
   c_File.Close();
   if(html_report==NULL || html_report=="")
      return false;

在下一个阶段, 搜索一个在报表文本中没有出现的字符, 并且可以用作分隔符。如果这样的字符不可用, 则以 "false" 结果从函数退出。

   string delimiter  =  NULL;
   ushort separate   =  0;
   for(uchar tr=1;tr<255;tr++)
     {
      string temp =  CharToString(tr);
      if(StringFind(html_report,temp,0)>0)
         continue;
      delimiter   =  temp;
      separate    =  tr;
      break;
     }
   if(delimiter==NULL)
      return false;

如上所述, 在 html 文件结构中, 表格由 "</table>" 封闭。我们用自己的分隔符替换这个标签, 并把完整的报表切分成数行。以这种方式, 我们抽取所需的表格至单独一行。

   if(StringReplace(html_report,"</table>",delimiter)<=0)
      return false;
   //---
   s_Symbol=NULL;
   car_Deals.Clear();
   //---
   string html_tables[];
   int size=StringSplit(html_report,separate,html_tables);
   if(size<=1)
      return false;

依据 "</tr>" 重复这个过程, 我们把表格分成几行。

   if(StringReplace(html_tables[size-2],"</tr>",delimiter)<=0)
      return false;
   size=StringSplit(html_tables[size-2],separate,html_tables);
   if(size<=1)
      return false;

现在让我们循环处理得到的字符串数组。首先, 遍历所有包含订单信息的字符串。此处, 我们将以文本 "Deals" 作为导向, 在交易报表 中它用来区分订单和交易。

   bool found_start=false;
   double opened_volume=0;
   for(int i=0;i<size;i++)
     {
      //---
      if(!found_start)
        {
         if(StringFind(html_tables[i],"Deals",0)>=0)
            found_start=true;
         continue;
        }

之后, 将每一行切分到单元格, 并将信息转换为相应的格式。

      string columns[];
      int temp=StringFind(html_tables[i],"<td>",0);
      if(temp<0)
         continue;
      if(temp>0)
         html_tables[i]=StringSubstr(html_tables[i],temp);
      StringReplace(html_tables[i],"<td>","");
      StringReplace(html_tables[i],"</td>",delimiter);
      temp=StringSplit(html_tables[i],separate,columns);
      if(temp<13)
         continue;
      //---
      ENUM_POSITION_TYPE   e_direction =  (ENUM_POSITION_TYPE)(columns[3]=="buy" ? POSITION_TYPE_BUY : columns[3]=="sell" ?
 POSITION_TYPE_SELL : -1);
      if(e_direction==-1)
         continue;
      //---
      datetime             dt_time     =  StringToTime(columns[0]);
      StringReplace(columns[5]," ","");
      double               d_volume    =  StringToDouble(columns[5]);
      StringReplace(columns[8]," ","");
      double               d_comission =  StringToDouble(columns[8]);
      StringReplace(columns[9]," ","");
      double               d_swap      =  StringToDouble(columns[9]);
      StringReplace(columns[10]," ","");
      double               d_profit    =  StringToDouble(columns[10]);
      if(s_Symbol==NULL || s_Symbol=="")
        {
         s_Symbol=columns[2];
         StringTrimLeft(s_Symbol);
         StringTrimRight(s_Symbol);
        }

在下一阶段, 检查交易是否为平仓操作。如果结果是肯定的, 则根据 FIFO 规则在我们的数据基础上平仓。

      if(opened_volume>0 && StringFind(columns[4],"out",0)>=0)
        {
         int total=car_Deals.Total();
         double total_volume=MathMin(opened_volume,d_volume);
         for(int d=0;(d<total && e_direction!=(-1) && total_volume>0);d++)
           {
            CDeal *deal=car_Deals.At(d);
            if(CheckPointer(deal)==POINTER_INVALID)
               continue;
            //---
            if(deal.Type()==e_direction)
               continue;
            //---
            double deal_unclosed=0;
            if(deal.IsClosed(deal_unclosed))
               continue;
            double close_volume     =  MathMin(deal_unclosed,total_volume);
            double close_comission  =  d_comission/d_volume*close_volume;
            double close_swap       =  d_swap/total_volume*close_volume;
            double close_profit     =  d_profit/total_volume*close_volume;
            if(deal.Close(close_volume,close_profit,close_comission,close_swap))
              {
               opened_volume  -= close_volume;
               d_volume       -= close_volume;
               total_volume   -= close_volume;
               d_comission    -= close_comission;
               d_swap         -= close_swap;
               d_profit       -= close_profit;
              }
           }
        }

然后检查是否进行了开仓操作。必要时, 在我们的数据基础上生成新交易。

      if(d_volume>0 && StringFind(columns[4],"in",0)>=0)
        {
         CDeal *deal = new CDeal(e_direction,dt_time,d_volume,d_comission);
         if(CheckPointer(deal)==POINTER_INVALID)
            return false;
         if(!car_Deals.Add(deal))
            return false;
         opened_volume  += d_volume;
        }
     }

如果至少有一笔交易被保存, 那么函数最终将返回 "true", 否则 - "false"。

   return (car_Deals.Total()>0);
  }

继续下一个工作阶段。

4. 准备与指标一起工作的类

正如我们之前已经说过的, 我们的一项任务是在缺乏明确定义的趋势下筛选亏损交易。趋势建立的问题会不时翻出来, 包括本网站 (例如, 文章 [3] 和 [4])。我不会假装发现了一些非凡的趋势建立方法。我只是想建议一种技术, 比较执行的交易与指标值, 以便后续分析并有意识的优化交易系统。所以, 我们来考察标准终端发行包中附带的最普遍的指标。

4.1. 包容 ATR 指标的类

首先考察振荡器类型指标 "平均真实范围"。正如我们所知, 在趋势中行情波动增加。这就是振荡器数值增长所代表的信号。我们需要保存哪些数值?迄今为止, 由于 EA 只在蜡烛开盘时分析设定的订单, 我建议我们应该保存最后一根收盘蜡烛的指标值, 以及该数值与前一根的比率。第一个数值表示当前的波动性, 第二个表示波动性交替的动态。

所考察的指标是典型的单缓冲区指标类别中的一个。因此, 对于我们来说, 用单一的类协同这种指标操作是有意义的。

保存指标数值的方法与保存交易的方法类似: 首先, 我们将创建一个类, 存储一笔交易的指标值, 然后根据外部请求和存储在数组中的数据, 创建一个可立即协同指标操作的上级类, 。

我们称第一个类为 "CValue"。它将包含 3 个私有变量, 用于存储有关的指标值 (Value), 指标 (Dinamic) 的最后两个值的比率, 和该数值所属的成交单号 (Deal_Ticket)。在分析过程中, 我们需要单号, 以便随后比较指标值与订单。所有需要的数值得以保存, 并将在初始化时传递给类实例。为了恢复所需的信息, 创建函数 GetTicket, GetValue 和 GetDinamic, 它们将返回相应的变量值。此外, 创建函数 GetValues 将同时返回指标值及其动态。

class CValue       : public CObject
  {
private:
   double            Value;            //指标值
   double            Dinamic;          //指标的动态值
   long              Deal_Ticket;      //成交单号 
   
public:
                     CValue(double value, double dinamic, long ticket);
                    ~CValue(void);
   //---
   long              GetTicket(void)   {  return Deal_Ticket;  }
   double            GetValue(void)    {  return Value;        }
   double            GetDinamic(void)  {  return Dinamic;      }
   void              GetValues(double &value, double &dinamic);
  };

然后, 生成上级类存储数据数组 COneBufferArray。在 "private" 模块中, 它将包含保存数据和指标句柄的数组。我要提醒一下, 我们已经决定创建一个通用类来处理所有的单缓冲区指标。但是调用不同的指标伴要随着一组不同的参数。所以在我看来, 最简单的变体就是在主程序中初始化一个指标, 并在类得以初始化之后再为其传递所需的指标句柄。为了随后的指标识别, 我们在报表中引入 "s_Name" 变量。

class COneBufferArray   :  CObject
  {
private:
   CArrayObj        *IndicatorValues;     //指标数值数组
   
   int               i_handle;            //指标句柄
   string            s_Name;
   string            GetIndicatorName(int handle);
   
public:
                     COneBufferArray(int handle);
                    ~COneBufferArray();
   //---
   bool              SaveNewValues(long ticket);
   //---
   double            GetValue(long ticket);
   double            GetDinamic(long ticket);
   bool              GetValues(long ticket, double &value, double &dinamic);
   int               GetIndyHandle(void)  {  return i_handle;     }
   string            GetName(void)        {  return (s_Name!= NULL ? s_Name : "...");       }
  };

通过外部请求保存数据, 创建 SaveNewValues 函数, 该函数将只包含一个参数 - 订单号。在函数开始时检查数据存储和指标句柄的数组状态。在出错的情况下, 函数将返回 "false" 值。

bool COneBufferArray::SaveNewValues(long ticket)
  {
   if(CheckPointer(IndicatorValues)==POINTER_INVALID)
      return false;
   if(i_handle==INVALID_HANDLE)
      return false;

之后, 我们将得到指标的数据。如果指标值下载失败, 函数将返回 false。

   double ind_buffer[];
   if(CopyBuffer(i_handle,0,1,2,ind_buffer)<2)
      return false;

在下一个步骤中创建 "CValue" 类实例, 并将所需数值传递给它。如果在创建类实例时发生错误, 函数将返回 false。

   CValue *object=new CValue(ind_buffer[1], (ind_buffer[0]!=0 ? ind_buffer[1]/ind_buffer[0] : 1), ticket);
   if(CheckPointer(object)==POINTER_INVALID)
      return false;

如果类不知道指标名称, 我们将调用函数 GetIndicatorName (函数代码在附件中提供) 从图表中得到它, 。

   if(s_Name==NULL)
      s_Name=GetIndicatorName(i_handle);

最后添加新创建的数据类实例, 并以所返回的操作结果退出函数。

   return IndicatorValues.Add(object);
  }

为了从请求中返回数组的数据, 创建函数 GetValue, GetDinamic 和 GetValues, 它将依据订单号返回所需数值。 

完整的类代码在附件中提供。

我层利用这个类来收集 CCI, Volumes, Force, Chaikin 振荡器和标准偏差等指标的数据。

4.2. 包容 MACD 指标的类

我们在集合中再添加一个标准指标 - MACD。正如我们所知, 它被用来研判趋势的力度和方向。

与之前考察的指标相比, MACD 有 2 个指标缓冲区 (主线和信号线)。因此, 我们也将保存两条线的信息。使用上面所示的指标算法, 数据存储的类代码如下所示:

class CMACDValue      : public CObject
  {
private:
   double            Main_Value;        //主线数值
   double            Main_Dinamic;      //主线的动态值
   double            Signal_Value;      //信号线数值
   double            Signal_Dinamic;    //信号线的动态值
   long              Deal_Ticket;       //成交单号
   
public:
                     CMACDValue(double main_value, double main_dinamic, double signal_value, double signal_dinamic, long ticket);
                    ~CMACDValue(void);
   //---
   long              GetTicket(void)         {  return Deal_Ticket;     }
   double            GetMainValue(void)      {  return Main_Value;      }
   double            GetMainDinamic(void)    {  return Main_Dinamic;    }
   double            GetSignalValue(void)    {  return Signal_Value;    }
   double            GetSignalDinamic(void)  {  return Signal_Dinamic;  }
   void              GetValues(double &main_value, double &main_dinamic, double &signal_value, double &signal_dinamic);
  };

处理数据数组的类也相应发生了变化。与 4.1 中描述的通用类相反, 这个类将使用一个特定的指标, 因此在类初始化时, 它不是将指标句柄传递给它, 而是它在初始化时所需的参数。指标初始化将在类中立即执行。

class CMACD
  {
private:
   CArrayObj        *IndicatorValues;     //指标数值数组
   
   int               i_handle;            //指标句柄
   
public:
                     CMACD(string symbol, ENUM_TIMEFRAMES timeframe, uint fast_ema, uint slow_ema, uint signal, ENUM_APPLIED_PRICE applied_price);
                    ~CMACD();
   //---
   bool              SaveNewValues(long ticket);
   //---
   double            GetMainValue(long ticket);
   double            GetMainDinamic(long ticket);
   double            GetSignalValue(long ticket);
   double            GetSignalDinamic(long ticket);
   bool              GetValues(long ticket, double &main_value, double &main_dinamic, double &signal_value, double &signal_dinamic);                
  };

函数的整体逻辑保持不变, 变化只针对指标缓冲区数量和保存的变量。

bool CMACD::SaveNewValues(long ticket)
  {
   if(CheckPointer(IndicatorValues)==POINTER_INVALID)
      return false;
   if(i_handle==INVALID_HANDLE)
      return false;
   double main[], signal[];
   if(!CopyBuffer(i_handle,0,1,2,main)<2 || !CopyBuffer(i_handle,1,1,2,signal)<2)
      return false;
   CMACDValue *object=new CMACDValue(main[1], (main[0]!=0 ? main[1]/main[0] : 1), signal[1], (signal[0]!=0 ? signal[1]/signal[0] : 1), ticket);
   if(CheckPointer(object)==POINTER_INVALID)
      return false;
   return IndicatorValues.Add(object);
  }

类似的伸缩逻辑适用于任何数量的指标缓冲区。如果您只想保存选定的指标缓冲区, 则只需在相应类的 SaveNewValues 函数中说明即可。不过, 现阶段我不建议这样做, 迄今我们还不知道盈利交易与某些指标缓冲区的数值之间是否有联系, 以及其程度。

为了整固材料, 这么说吧, 我们再次展示一个使用 3 个数据缓冲区保存指标数据的例子。

4.3. 包容 ADZ 指标的类

ADX 指标被广泛用于判定趋势的力度和方向。它与我们的任务一致, 并正确地加入到我们的 "钱箱" 中。

该指标有 3 个指标缓冲区, 根据上述建议的伸缩方法, 我们增加了变量保存数量。因此, 数据存储类将如下所示:

class CADXValue      : public CObject
  {
private:
   double            ADX_Value;        //ADX 数值
   double            ADX_Dinamic;      // ADX 动态数值
   double            PDI_Value;        //+DI 数值
   double            PDI_Dinamic;      // +DI 的动态数值
   double            NDI_Value;        //-DI 数值
   double            NDI_Dinamic;      // -DI 的动态数值
   long              Deal_Ticket;      //成交单号 
   
public:
                     CADXValue(double adx_value, double adx_dinamic, double pdi_value, double pdi_dinamic, double ndi_value, double ndi_dinamic, long ticket);
                    ~CADXValue(void);
   //---
   long              GetTicket(void)         {  return Deal_Ticket;     }
   double            GetADXValue(void)       {  return ADX_Value;       }
   double            GetADXDinamic(void)     {  return ADX_Dinamic;     }
   double            GetPDIValue(void)       {  return PDI_Value;       }
   double            GetPDIDinamic(void)     {  return PDI_Dinamic;     }
   double            GetNDIValue(void)       {  return NDI_Value;       }
   double            GetNDIDinamic(void)     {  return NDI_Dinamic;     }
   void              GetValues(double &adx_value, double &adx_dinamic, double &pdi_value, double &pdi_dinamic, double &ndi_value, double &ndi_dinamic);
  };

 增加存储数据意味着需要在类中进行修改以便处理数组。

class CADX
  {
private:
   CArrayObj        *IndicatorValues;     //指标数值数组
   
   int               i_handle;            //指标句柄
   
public:
                     CADX(string symbol, ENUM_TIMEFRAMES timeframe, uint period);
                    ~CADX();
   //---
   bool              SaveNewValues(long ticket);
   //---
   double            GetADXValue(long ticket);
   double            GetADXDinamic(long ticket);
   double            GetPDIValue(long ticket);
   double            GetPDIDinamic(long ticket);
   double            GetNDIValue(long ticket);
   double            GetNDIDinamic(long ticket);
   bool              GetValues(long ticket,double &adx_value,double &adx_dinamic,double &pdi_value,double &pdi_dinamic,double &ndi_value,double &ndi_dinamic);
  };
bool CADX::SaveNewValues(long ticket)
  {
   if(CheckPointer(IndicatorValues)==POINTER_INVALID)
      return false;
   if(i_handle==INVALID_HANDLE)
      return false;
   double adx[], pdi[], ndi[];
   if(!CopyBuffer(i_handle,0,1,2,adx)<2 || !CopyBuffer(i_handle,1,1,2,pdi)<2 || !CopyBuffer(i_handle,1,1,2,ndi)<2)
      return false;
   CADXValue *object=new CADXValue(adx[1], (adx[0]!=0 ? adx[1]/adx[0] : 1), pdi[1], (pdi[0]!=0 ? pdi[1]/pdi[0] : 1), ndi[1], (ndi[0]!=0 ? ndi[1]/ndi[0] : 1), ticket);
   if(CheckPointer(object)==POINTER_INVALID)
      return false;
   return IndicatorValues.Add(object);
  }

我相信现在大家都明白为了协同指标工作, 如何构建类的原则。所以, 我们不会赘述以下指标代码以便节省文章篇幅。类似地, 对于 "钱箱" 的分析, 我添加了比尔·威廉姆斯的资金流指数和鳄鱼。所有人都愿意从附件中得到完整的类代码以便熟练掌握它。

5. 为结果输出准备报表表格

从交易产生时刻的相关指标获取信息后, 我们应该考虑对获得的数据进行分析。在我看来, 最清晰的莫过于建立交易利润与各指标值的依赖关系图表。我提议根据文章 [2] 中建议的 Victor 技术来构建图表。

我来做些保留: 迄今我实现的交易优化当中, 我会在指标值上搜索利润依赖关系。如果读者尝试重复任何交易, 他需要搜索交易数量和指标值之间的依赖关系。

首先, 创建在每个指标上准备信息的类。

5.1. 单缓冲区指标的通用类

首先创建一个使用单缓冲区指标的类。我们可以分析哪些信息?请记住, 我们保存了指标缓冲区的数值, 和其交替的动态。所以, 我们可以分析:

  • 盈利与开仓时刻指标值的依赖性,
  • 指标线走势对盈利的影响,
  • 还有, 指标值及其动态对操作执行结果的复杂影响。

对于图表绘制, 创建 CStaticOneBuffer 类。这个类将包含已保存数据数组的引用 - DataArray, 预设步长 d_Step 的 Value 指标的数据数组, 以及分别保存多头仓位和空头仓位总利润的两个数组。注意: 用于计算总利润的数组是二维的。第一次测量的大小将对应于 Value 数组的大小。第二次测量将包含三个元素: 第一个 - 下跌指标动态, 第二个 - 横向指标走势, 第三个 - 上涨走势。

在类初始化时, 在参数中设置指向数据数组的引用, 和指标数值的步长。

class CStaticOneBuffer  :  CObject
  {
private:
   COneBufferArray  *DataArray;
   
   double            d_Step;                    //数据数组的步长
   double            Value[];                   //数据数组
   double            Long_Profit[][3];          //多头交易盈利, 指向 -> 下降-0,相等-1, 上升-2
   double            Short_Profit[][3];         //空头交易盈利, 指向 -> 指向 -> 下降-0,相等-1, 上升-2
   
   bool              AdValues(double value, double dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticOneBuffer(COneBufferArray *data, double step);
                    ~CStaticOneBuffer();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

在初始化函数中, 保存传递的值并将正在使用的数组归零。

CStaticOneBuffer::CStaticOneBuffer(COneBufferArray *data,double step)
  {
   DataArray   =  data;
   d_Step      =  step;
   ArrayFree(Value);
   ArrayFree(Long_Profit);
   ArrayFree(Short_Profit);
  }

为了收集统计信息, 创建 Ad 函数传递一笔交易相关的信息。各个指标参数将在函数内部, 数据将被保存到所需的数组元素中。

bool CStaticOneBuffer::Ad(long ticket,double profit,ENUM_POSITION_TYPE type)
  {
   if(CheckPointer(DataArray)==POINTER_INVALID)
      return false;

   double value, dinamic;
   if(!DataArray.GetValues(ticket,value,dinamic))
      return false;
   value = NormalizeDouble(value/d_Step,0)*d_Step;
   return AdValues(value,dinamic,profit,type);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CStaticOneBuffer::AdValues(double value,double dinamic,double profit,ENUM_POSITION_TYPE type)
  {
   int index=GetIndex(value);
   if(index<0)
      return false;
   
   switch(type)
     {
      case POSITION_TYPE_BUY:
        if(dinamic<1)
           Long_Profit[index,0]+=profit;
        else
           if(dinamic==1)
              Long_Profit[index,1]+=profit;
           else
              Long_Profit[index,2]+=profit;
        break;
      case POSITION_TYPE_SELL:
        if(dinamic<1)
           Short_Profit[index,0]+=profit;
        else
           if(dinamic==1)
              Short_Profit[index,1]+=profit;
           else
              Short_Profit[index,2]+=profit;
        break;
     }
   return true;
  }

对于图表可视化, 创建函数 HTML_header 和 HTML_body, 它们是 HTML-page 的代码片段, 将生成标题和正文。在文章 [2] 中详细介绍了构建 HTML 页面代码的原则, 我们无需关注它。完整的函数代码在附件中提供。

5.2. 显示比尔·威廉姆斯资金流指数指标数据的类

接下来我们来考察比尔·威廉姆斯资金流指数指标。通过图表显示的方法, 它类似于单缓冲区指标, 但有一个区别: 比尔·威廉姆斯资金流指数也有调色板缓冲区, 它也有一个值。与此同时, 与双缓冲区指标相反, 我们对颜色缓冲区的变化动态不感兴趣。所以, 对于上述建议的单缓冲区指标的图表, 将增加利润与指标颜色的依赖性图表, 以及当前指标颜色对于指标动态影响的复杂图表。

为了收集统计数据和创建分析图表, 创建 CStaticBWMFI 类。类结构与上面考察过的类似。变化涉及计算利润的数组, 现在它们有三个维度。第三维根据使用的颜色数量获得 4 个元素。

class CStaticBWMFI  :  CObject
  {
private:
   CBWMFI           *DataArray;
   
   double            d_Step;                       //数据数组的步长
   double            Value[];                      //数据数组
   double            Long_Profit[][3][4];          //多头交易盈利, 指向 -> 下降-0, 相等-1, 上升-2
   double            Short_Profit[][3][4];         //空头交易盈利, 指向 -> 下降-0, 相等-1, 上升-2
   
   bool              AdValues(double value, double _color, double dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticBWMFI(CBWMFI *data, double step);
                    ~CStaticBWMFI();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

完整的类代码在附件中提供。

5.3. 显示 MACD 指标数据的类

再一个, 我们考察 MACD 指标。如您所知, 它有两个缓冲区: 直方条和信号线。根据该指标信号的解释规则, 直方条值和走势方向是重要的, 信号线的位置 (直方条上方或下方) 也同样重要。为了全面分析, 我们将要创建一些图表。

  • 交易的盈利因子依赖于直方条数值及其方向 (分别且复杂)。
  • 交易的盈利因子依赖于信号线数值及其方向。
  • 利润依赖于信号线相对于直方条的位置。
  • 利润依赖于直方条数值、其方向以及信号线相对于直方条的位置等因素的联合效应。
为了数据分析, 创建 CStaticMACD 类。当构建类时, 遵循的原则与创建之前的统计类相同。通过直方条数值得到三维的利润统计数据, 但与之前的类相比, 第三维将根据信号线相对于直方条的位置 (较低, 同级和较高) 包含 3 个元素。另外, 依据信号线的数值添加另一个二维数组来计算利润。

class CStaticMACD  :  CObject
  {
private:
   CMACD            *DataArray;
   
   double            d_Step;                       //数据数组的步长
   double            Value[];                      //数据数组
   double            SignalValue[];                //数据数组
   double            Long_Profit[][3][3];          //多头交易盈利, 指向 -> 下降-0, 相等-1, 上升-2
   double            Short_Profit[][3][3];         //空头交易盈利, 指向 -> 下降-0, 相等-1, 上升-2
   double            Signal_Long_Profit[][3];      //多头交易盈利, 指向 -> 下降-0, 相等-1, 上升-2
   double            Signal_Short_Profit[][3];     //空头交易盈利, 指向 -> 下降-0, 相等-1, 上升-2
   
   bool              AdValues(double main_value, double main_dinamic, double signal_value, double signal_dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetIndex(double value);
   int               GetSignalIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticMACD(CMACD *data, double step);
                    ~CStaticMACD();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

如您所见, 类结构, 名称和函数职能保持不变。变化仅涉及函数的内容, 您可以在附件中获取并熟悉它。

5.4. 显示 ADX 指标数据的类

下一个要考察的是 CStaticADX 类。它将通过 ADX 指标的数值收集统计数据。指标信号解读规则: 线 +DI 表示正向走势力度, -DI — 负向走势力度, ADX — 中间走势力度。从这些规则出发, 我们将建立依赖关系图表:

  • 利润依赖于 +DI 的数值, 相对于 ADX 的方向和位置。
  • 利润依赖于 -DI 的数值, 相对于 ADX 的方向和位置。

创建收集统计数据的类, 我决定多收集一点数据。作为结果, 我需要保存的信息是关于:

  • 指标值;
  • 曲线方向;
  • 相对于反向走势曲线的位置;
  • 反向走势曲线的方向;
  • 相对于 ADX 曲线的位置;
  • ADX 的方向。
由于这种信息片段, 和以前的类中使用的方法, 我需要六维的数组。但是在 MQL 中不支持这种尺寸的数组。为了解决此任务, 将创建 CProfitData 辅助类, 在其中保存所有必需的信息。

class CProfitData
  {
   public:
   double         Value;
   double         LongProfit[3]/*UppositePosition*/[3]/*Upposite Direct*/[3]/*ADX position*/[3]/*ADX direct*/;
   double         ShortProfit[3]/*UppositePosition*/[3]/*Upposite Direct*/[3]/*ADX position*/[3]/*ADX direct*/;
   
                  CProfitData(void) 
                  {  ArrayInitialize(LongProfit,0); ArrayInitialize(ShortProfit,0);  }
                 ~CProfitData(void) {};
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CStaticADX  :  CObject
  {
private:
   CADX             *DataArray;
   
   double            d_Step;           //数据数组的步长
   CProfitData      *PDI[][3];         // +DI 数据数组
   CProfitData      *NDI[][3];         // -DI 数据数组
   
   bool              AdValues(double adx_value, double adx_dinamic, double pdi_value, double pdi_dinamic, double ndi_value, double ndi_dinamic, double profit, ENUM_POSITION_TYPE type);
   int               GetPDIIndex(double value);
   int               GetNDIIndex(double value);
   bool              Sort(void);
   
public:
                     CStaticADX(CADX *data, double step);
                    ~CStaticADX();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

在其它方面, 保留了以前构建类的方法和原则。完整的类代码在附件中提供。

5.5. 显示鳄鱼指标数据的类

在这个板块的最后, 我们创建一个用于收集鳄鱼指标统计数据的类。这个指标的信号是基于不同时期的三条移动平均线。因此, 在解释指标信号时, 某些指标线的数值对我们来说无关紧要。更重要的是线的方向和位置。

为了使指标信号更特别, 让我们引入通过曲线的位置判断趋势。如果唇线高于牙线, 而牙线高于颚线 — 可视为买入趋势。如果唇线低于牙线, 而牙线低于颚线 — 可视为卖出趋势。在指示线缺乏严格的顺序时, 则视为趋势不明或横盘。

分别从趋势方向信号和指标线动态构建依赖图表。

按照上面指定的输入数据创建 CStaticAlligator 类。类的构建原则来自以前的类。

class CStaticAlligator  :  CObject
  {
private:
   CAlligator             *DataArray;
   
   double            Long_Profit[3]/*Signal*/[3]/*JAW direct*/[3]/*TEETH direct*/[3]/*LIPS direct*/;  //Array of long deals profit
   double            Short_Profit[3]/*Signal*/[3]/*JAW direct*/[3]/*TEETH direct*/[3]/*LIPS direct*/; //Array of short feals profit
   
   bool              AdValues(double jaw_value, double jaw_dinamic, double teeth_value, double teeth_dinamic, double lips_value, double lips_dinamic, double profit, ENUM_POSITION_TYPE type);
   
public:
                     CStaticAlligator(CAlligator *data);
                    ~CStaticAlligator();
   bool              Ad(long ticket, double profit, ENUM_POSITION_TYPE type);
   string            HTML_header(void);
   string            HTML_body(void);
  };

完整的类代码在附件中提供。

6. 构建信息收集和分析的 EA

现在, 当所有的工作准备就绪之后, 我们将创建一个 EA, 在策略测试器中立即启动它, 收集信息并输出分析数据。首先, 在 EA 输入参数中指定用于分析的测试报表文件名, 使用的时间帧以及所有指标所需的参数。

input string            FileName          =  "Kalman_test.html"   ;
input ENUM_TIMEFRAMES   Timefarame        =  PERIOD_CURRENT       ;
input string            s1                =  "ADX"                ;  //---
input uint              ADX_Period        =  14                   ;
input string            s2                =  "Alligator"          ;  //---
input uint              JAW_Period        =  13                   ;
input uint              JAW_Shift         =  8                    ;
input uint              TEETH_Period      =  8                    ;
input uint              TEETH_Shift       =  5                    ;
input uint              LIPS_Period       =  5                    ;
input uint              LIPS_Shift        =  3                    ;
input ENUM_MA_METHOD    Alligator_Method  =  MODE_SMMA            ;
input ENUM_APPLIED_PRICE Alligator_Price  =  PRICE_MEDIAN         ;
input string            s3                =  "ATR"                ;  //---
input uint              ATR_Period        =  14                   ;
input string            s4                =  "BW MFI"             ;  //---
input ENUM_APPLIED_VOLUME BWMFI_Volume    =  VOLUME_TICK          ;
input string            s5                =  "CCI"                ;  //---
input uint              CCI_Period        =  14                   ;
input ENUM_APPLIED_PRICE CCI_Price        =  PRICE_TYPICAL        ;
input string            s6                =  "Chaikin"            ;  //---
input uint              Ch_Fast_Period    =  3                    ;
input uint              Ch_Slow_Period    =  14                   ;
input ENUM_MA_METHOD    Ch_Method         =  MODE_EMA             ;
input ENUM_APPLIED_VOLUME Ch_Volume       =  VOLUME_TICK          ;
input string            s7                =  "Force Index"        ;  //---
input uint              Force_Period      =  14                   ;
input ENUM_MA_METHOD    Force_Method      =  MODE_SMA             ;
input ENUM_APPLIED_VOLUME Force_Volume    =  VOLUME_TICK          ;
input string            s8                =  "MACD"               ;  //---
input uint              MACD_Fast         =  12                   ;
input uint              MACD_Slow         =  26                   ;
input uint              MACD_Signal       =  9                    ;
input ENUM_APPLIED_PRICE MACD_Price       =  PRICE_CLOSE          ;
input string            s9                =  "Standart Deviation" ;  //---
input uint              StdDev_Period     =  14                   ;
input uint              StdDev_Shift      =  0                    ;
input ENUM_MA_METHOD    StdDev_Method     =  MODE_SMA             ;
input ENUM_APPLIED_PRICE StdDev_Price     =  PRICE_CLOSE          ;
input string            s10               =  "Volumes"            ;  //---
input ENUM_APPLIED_VOLUME Applied_Volume  =  VOLUME_TICK          ;

然后声明上述所有类的实例。

CArrayObj         *Deals;
CADX              *ADX;
CAlligator        *Alligator;
COneBufferArray   *ATR;
CBWMFI            *BWMFI;
COneBufferArray   *CCI;
COneBufferArray   *Chaikin;
COneBufferArray   *Force;
CMACD             *MACD;
COneBufferArray   *StdDev;
COneBufferArray   *Volume;
CStaticOneBuffer  *IndicatorsStatic[];
CStaticBWMFI      *BWMFI_Stat;
CStaticMACD       *MACD_Stat;
CStaticADX        *ADX_Stat;
CStaticAlligator  *Alligator_Stat;

6.1. EA 初始化函数

我们设计用于分析数据的 EA, 在策略测试器中, 启动时首先检查环境。如果并非在测试器中开始, 则其初始化必须终止。

int OnInit()
  {
//---
   if(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
      return INIT_FAILED;

然后从测试报表文件中解析数据。从报表中读取数据之后, 不再需要解析类实例, 我们将其从内存中删除。

   CParsing *Parsing =  new CParsing(Deals);
   if(CheckPointer(Parsing)==POINTER_INVALID)
      return INIT_FAILED;
   if(!Parsing.ReadFile(FileName) || CheckPointer(Deals)==POINTER_INVALID || Deals.Total()<=0)
     {
      delete Parsing;
      return INIT_FAILED;
     }
   delete Parsing;

之后, 执行指标类的初始化。

//---
   ADX =  new CADX(_Symbol,Timefarame,ADX_Period);
   if(CheckPointer(ADX)==POINTER_INVALID)
      return INIT_FAILED;
//---
   Alligator =  new CAlligator(_Symbol,Timefarame,JAW_Period,JAW_Shift,TEETH_Period,TEETH_Shift,LIPS_Period,LIPS_Shift,Alligator_Method,Alligator_Price);
   if(CheckPointer(Alligator)==POINTER_INVALID)
      return INIT_FAILED;
//---
   int handle=iATR(_Symbol,Timefarame,ATR_Period);
   if(handle>0)
     {
      ATR      =  new COneBufferArray(handle);
      if(CheckPointer(ATR)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   BWMFI    =  new CBWMFI(_Symbol,Timefarame,BWMFI_Volume);
   if(CheckPointer(BWMFI)==POINTER_INVALID)
      return INIT_FAILED;
//---
   handle=iCCI(_Symbol,Timefarame,CCI_Period,CCI_Price);
   if(handle>0)
     {
      CCI      =  new COneBufferArray(handle);
      if(CheckPointer(CCI)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   handle=iChaikin(_Symbol,Timefarame,Ch_Fast_Period,Ch_Slow_Period,Ch_Method,Ch_Volume);
   if(handle>0)
     {
      Chaikin  =  new COneBufferArray(handle);
      if(CheckPointer(Chaikin)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   handle=iForce(_Symbol,Timefarame,Force_Period,Force_Method,Force_Volume);
   if(handle>0)
     {
      Force    =  new COneBufferArray(handle);
      if(CheckPointer(Force)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   MACD     =  new CMACD(_Symbol,Timefarame,MACD_Fast,MACD_Slow,MACD_Signal,MACD_Price);
   if(CheckPointer(MACD)==POINTER_INVALID)
      return INIT_FAILED;
//---
   handle=iStdDev(_Symbol,Timefarame,StdDev_Period,StdDev_Shift,StdDev_Method,StdDev_Price);
   if(handle>0)
     {
      StdDev   =  new COneBufferArray(handle);
      if(CheckPointer(StdDev)==POINTER_INVALID)
         return INIT_FAILED;
     }
//---
   handle=iVolumes(_Symbol,Timefarame,Applied_Volume);
   if(handle>0)
     {
      Volume   =  new COneBufferArray(handle);
      if(CheckPointer(Volume)==POINTER_INVALID)
         return INIT_FAILED;
     }

在 OnInit 函数结束时, 将计数器设置为 0 并退出函数。

   cur_ticket   =  0;
//---
   return(INIT_SUCCEEDED);
  }

6.2. 收集统计数据

在 OnTick 函数中将执行收集有关指标状态的数据。在函数开始时检查是否所有订单的信息均已收集齐全。如果是, 退出该函数。

void OnTick()
  {
   if(cur_ticket>=Deals.Total())
      return;

在接下来的步骤中, 交易分析的执行时间将与分笔报价的处理时间进行比较。如果交易时间还没有到, 退出该函数。

   CDeal *object  =  Deals.At(cur_ticket);
   if(object.GetTime()>TimeCurrent())
      return;

如果前次检查已通过, 检查指标类实例的状态, 并调用 SaveNewValues 函数为每个指标类保存所需的信息。

   if(CheckPointer(ADX)!=POINTER_INVALID)
      ADX.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Alligator)!=POINTER_INVALID)
      Alligator.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(ATR)!=POINTER_INVALID)
      ATR.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(BWMFI)!=POINTER_INVALID)
      BWMFI.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(CCI)!=POINTER_INVALID)
      CCI.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Chaikin)!=POINTER_INVALID)
      Chaikin.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Force)!=POINTER_INVALID)
      Force.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(MACD)!=POINTER_INVALID)
      MACD.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(StdDev)!=POINTER_INVALID)
      StdDev.SaveNewValues(cur_ticket);
   //---
   if(CheckPointer(Volume)!=POINTER_INVALID)
      Volume.SaveNewValues(cur_ticket);

在该函数的末尾, 增加已处理订单的计数器并退出该函数。

   cur_ticket++;
   return;
  }

6.3. 分析图表输出

数据分析和报表输出将在 OnTester 函数中执行。启动时, 函数检查交易数量以便进行分析。

double OnTester()
  {
   double ret=0.0;
   int total=Deals.Total();

如果有必要进行分析, 则执行统计类的初始化。

为了便于后续处理, 将单缓冲区指标的统计类收集到数组中。所以, 初始化同时并行计数单缓冲区指标。

   int total_indy=0;
   if(total>0)
     {
      if(CheckPointer(ADX)!=POINTER_INVALID)
         ADX_Stat=new CStaticADX(ADX,1);
      //---
      if(CheckPointer(Alligator)!=POINTER_INVALID)
         Alligator_Stat=new CStaticAlligator(Alligator);
      //---
      if(CheckPointer(ATR)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(ATR,_Point*10);
         if(CheckPointer(indy)!=POINTER_INVALID)
           {
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
           }
        }
      //---
      if(CheckPointer(BWMFI)!=POINTER_INVALID)
         BWMFI_Stat=new CStaticBWMFI(BWMFI,_Point*100);
      //---
      if(CheckPointer(CCI)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(CCI,10);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(Chaikin)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(Chaikin,100);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(Force)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(Force,0.1);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(MACD)!=POINTER_INVALID)
         MACD_Stat=new CStaticMACD(MACD,_Point*10);
      //---
      if(CheckPointer(StdDev)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(StdDev,_Point*10);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
      //---
      if(CheckPointer(Volume)!=POINTER_INVALID)
        {
         CStaticOneBuffer *indy=new CStaticOneBuffer(Volume,100);
         if(CheckPointer(indy)!=POINTER_INVALID)
            if(ArrayResize(IndicatorsStatic,total_indy+1)>0)
              {
               IndicatorsStatic[total_indy]=indy;
               total_indy++;
              }
        }
     }

此外, 将指标数据与各笔交易进行比较, 并依据图形报表所需的输出方向将信息分组。为此目的, 在每个统计类中调用 Ad 函数, 在其参数中传递有关交易信息。

   for(int i=0;i<total;i++)
     {
      CDeal               *deal     =  Deals.At(i);
      ENUM_POSITION_TYPE   type     =  deal.Type();
      double               d_profit =  deal.GetProfit();
      
      for(int ind=0;ind<total_indy;ind++)
         IndicatorsStatic[ind].Ad(i,d_profit,type);
      if(CheckPointer(BWMFI_Stat)!=POINTER_INVALID)
         BWMFI_Stat.Ad(i,d_profit,type);
      if(CheckPointer(MACD_Stat)!=POINTER_INVALID)
         MACD_Stat.Ad(i,d_profit,type);
      if(CheckPointer(ADX_Stat)!=POINTER_INVALID)
         ADX_Stat.Ad(i,d_profit,type);
      if(CheckPointer(Alligator_Stat)!=POINTER_INVALID)
         Alligator_Stat.Ad(i,d_profit,type);
     }

数据分组后创建一个报表文件 Report.html, 并将其保存在终端的共享文件夹中。

   if(total_indy>0 || CheckPointer(BWMFI_Stat)!=POINTER_INVALID || CheckPointer(MACD_Stat)!=POINTER_INVALID
      || CheckPointer(ADX_Stat)!=POINTER_INVALID || CheckPointer(Alligator_Stat)!=POINTER_INVALID )
     {
      int handle=FileOpen("Report.html",FILE_WRITE|FILE_TXT|FILE_COMMON);
      if(handle<0)
         return ret;

在开始处写入我们的 html-report 文件头。

      FileWrite(handle,"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">");
      FileWrite(handle,"<html> <head> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
      FileWrite(handle,"<title>Deals to Indicators</title> <!-- - -->");
      FileWrite(handle,"<script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js\" type=\"text/javascript\"></script>");
      FileWrite(handle,"<script src=\"https://code.highcharts.com/highcharts.js\" type=\"text/javascript\"></script>");
      FileWrite(handle,"<!-- - --> <script type=\"text/javascript\">$(document).ready(function(){");

然后, 逐一调用所有统计类的 HTML_header 函数, 输入我们的文件数据以便绘制图表。

      for(int ind=0;ind<total_indy;ind++)
         FileWrite(handle,IndicatorsStatic[ind].HTML_header());
      if(CheckPointer(BWMFI_Stat)!=POINTER_INVALID)
         FileWrite(handle,BWMFI_Stat.HTML_header());
      if(CheckPointer(MACD_Stat)!=POINTER_INVALID)
         FileWrite(handle,MACD_Stat.HTML_header());
      if(CheckPointer(ADX_Stat)!=POINTER_INVALID)
         FileWrite(handle,ADX_Stat.HTML_header());
      if(CheckPointer(Alligator_Stat)!=POINTER_INVALID)
         FileWrite(handle,Alligator_Stat.HTML_header());

之后, 逐一调用每个统计类的 HTML_body 函数创建一个报表输出模板。注意: 通过调用这个函数, 我们就完成了统计类的工作, 并删除它来清理内存。

      FileWrite(handle,"});</script> <!-- - --> </head> <body>");
      for(int ind=0;ind<total_indy;ind++)
        {
         FileWrite(handle,IndicatorsStatic[ind].HTML_body());
         delete IndicatorsStatic[ind];
        }
      if(CheckPointer(BWMFI_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,BWMFI_Stat.HTML_body());
         delete BWMFI_Stat;
        }
      if(CheckPointer(MACD_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,MACD_Stat.HTML_body());
         delete MACD_Stat;
        }
      if(CheckPointer(ADX_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,ADX_Stat.HTML_body());
         delete ADX_Stat;
        }
      if(CheckPointer(Alligator_Stat)!=POINTER_INVALID)
        {
         FileWrite(handle,Alligator_Stat.HTML_body());
         delete Alligator_Stat;
        }

在末尾, 写入结束标签, 关闭文件, 清除数组并退出函数。

      FileWrite(handle,"</body> </html>");
      FileFlush(handle);
      FileClose(handle);
     }
//---
   ArrayFree(IndicatorsStatic);
//---
   return(ret);
  }

不要忘记在 OnDeinit 函数中删除残余的类。

7. 信息分析

我们的工作正迎来逻辑上的结局。现在到我们该看看结果的时候了。为了做到这一点, 返回策略测试器, 使用所有设置重复测试我们在文章第二部分研究的智能系统, 并开始测试我们新创建的分析 EA。

测试完成后, 打开共享终端文件夹, 找到 Report.html。在浏览器中打开它。进而, 我将举出的例子来自我的报表。

7.1. ATR

ATR 报表

在分析利润对 ATR 指标的依赖关系图表时, 我没看到潜在盈利的区域, 因此不存在交易过滤的可能性。

7.2. CCI

CCI 报表

利润对 CCI 指标的依赖关系图表, 当指标值高于 200, 且指标线增长时, 可由买入交易赚取一些利润。但却没有可盈利的卖出交易区域。

7.3. Chaikin

Chaikin 报表

Chaikin 振荡器就像 ATR, 没有体现出指标值与交易利润之间的联系。

7.4. 力度指标

力度指标报表

力度指标的分析图表也没有显示任何依赖关系。

7.5. 标准偏差

SndDev 报表

分析 StdDev 指标值的依赖关系, 显示有部分区域可以买入交易, 但缺乏卖出交易的可能性。

7.6. 交易量指标

利润依赖于交易量。

我们在交易量指标数据分析中没有检测到依赖性。

7.7. 比尔·威廉姆斯资金流指数

比尔·威廉姆斯资金流指数

比尔·威廉姆斯资金流指数指标, 如果仅根据 #0 颜色缓冲区开单, 则过滤买入交易可获得利润。但我们未能检测到任何对卖出交易的依赖。

7.8. MACD

MACD 报表MACD 报表

MACD 指标的信号可过滤盈利的买入交易。如果您在信号线位于直方条上方时下单买入, 则有可能盈利。但分析并没有显示可盈利的卖出区域。与此同时, 该指标能减少亏损操作, 例如在直方条不断增长, 以及信号线低于或等于直方条时能够排除卖出操作。

7.9. ADX

ADX 指标信号的分析表明无法过滤交易。

7.10. 鳄鱼

鳄鱼报表鳄鱼报表

在我看来, 利用鳄鱼指标进行交易过滤是最有前途的。可由指示线位置和方向的组合中找到进行交易的形态。因此, 如果符合以下条件, 执行买入交易也许会盈利:

  • 指标线位置表现出空头趋势, 唇线或颚线反转上行;
  • 指标线位置表现出多头趋势, 唇线和牙线指向上行;
  • 趋势不明朗, 齿线和颚线指向下行。 

对于卖出交易, 使用镜像信号。

8. 调整初始 EA

我们在分析 EA 的交易时进行了非常广泛的工作。现在, 我们来看看这将如何影响我们的策略表现。为此目的, 根据以上具体的分析, 修改来自文章 [1] 的交易信号模块, 添加具有过滤规则的指标。我建议将 MACD 和鳄鱼加入我们的模块。

我建议按顺序添加指标滤波器, 并在添加每个滤波器后循环执行将交易解析至指标的过程。这可更清楚地理解每个滤波器对整个策略的影响, 并有助于评估它们的复杂影响。因此, 如果第一阶段的分析不能检测到利润对任何指标值的依赖性, 那么在随后的迭代中您就根本不会再看到这种依赖性。我现在不做这些, 只是不希望文章篇幅过于膨胀。

首先, 将指标参数添加到模块描述中。

//| Parameter=JAW_Period,uint,13,JAW Period                                   |
//| Parameter=JAW_Shift,uint,8,JAW Shift                                      |
//| Parameter=TEETH_Period,uint,8,TEETH Period                                |
//| Parameter=TEETH_Shift,uint,5,TEETH Shift                                  |
//| Parameter=LIPS_Period,uint,5,LIPS Period                                  |
//| Parameter=LIPS_Shift,uint,3,LIPS_Shift                                    |
//| Parameter=Alligator_Method,ENUM_MA_METHOD,MODE_SMMA,Method                |
//| Parameter=Alligator_Price,ENUM_APPLIED_PRICE,PRICE_MEDIAN,Alligator Price |
//| Parameter=MACD_Fast,uint,12,MACD Fast                                     |
//| Parameter=MACD_Slow,uint,26,MACD Slow                                     |
//| Parameter=MACD_Signal,uint,9,MACD Signal                                  |
//| Parameter=MACD_Price,ENUM_APPLIED_PRICE,PRICE_CLOSE,MACD Price            |


在私有模块中添加存储参数的变量, 而在公有模块中添加保存它们的函数。

   uint              ci_MACD_Fast;
   uint              ci_MACD_Slow;
   uint              ci_MACD_Signal;
   ENUM_APPLIED_PRICE ce_MACD_Price;
   uint              ci_JAW_Period;
   uint              ci_JAW_Shift;
   uint              ci_TEETH_Period;
   uint              ci_TEETH_Shift;
   uint              ci_LIPS_Period;
   uint              ci_LIPS_Shift;
   ENUM_MA_METHOD    ce_Alligator_Method;
   ENUM_APPLIED_PRICE ce_Alligator_Price;
   void              JAW_Period(uint value)                 {  ci_JAW_Period  =  value;   }
   void              JAW_Shift(uint value)                  {  ci_JAW_Shift   =  value;   }
   void              TEETH_Period(uint value)               {  ci_TEETH_Period=  value;   }
   void              TEETH_Shift(uint value)                {  ci_TEETH_Shift =  value;   }
   void              LIPS_Period(uint value)                {  ci_LIPS_Period =  value;   }
   void              LIPS_Shift(uint value)                 {  ci_LIPS_Shift  =  value;   }
   void              Alligator_Method(ENUM_MA_METHOD value) {  ce_Alligator_Method  =  value;   }
   void              Alligator_Price(ENUM_APPLIED_PRICE value) {  ce_Alligator_Price=  value;   }
   void              MACD_Fast(uint value)                  {  ci_MACD_Fast   =  value;   }
   void              MACD_Slow(uint value)                  {  ci_MACD_Slow   =  value;   }
   void              MACD_Signal(uint value)                {  ci_MACD_Signal =  value;   }
   void              MACD_Price(ENUM_APPLIED_PRICE value)   {  ce_MACD_Price  =  value;   }

还有, 我们必须加入处理指标的类, 以及必要数据的接收和初始化函数。为了处理 MACD, 我用了一个标准类。迄今为止, 针对鳄鱼的标准类并不存在, 我用三个移动平均线类代替它, 根据指标线的名称为它们分配名称。

protected:
   CiMACD            m_MACD;           // 振荡器对象
   CiMA              m_JAW;
   CiMA              m_TEETH;
   CiMA              m_LIPS;
     
   //--- 指标的初始化方法
   bool              InitMACD(CIndicators *indicators);
   bool              InitAlligator(CIndicators *indicators);
   //--- 获取数据的方法
   double            Main(int ind)                     { return(m_MACD.Main(ind));      }
   double            Signal(int ind)                   { return(m_MACD.Signal(ind));    }
   double            DiffMain(int ind)                 { return(Main(ind+1)!=0 ? Main(ind)-Main(ind+1) : 0); }
   int               AlligatorTrend(int ind);
   double            DiffJaw(int ind)                  { return(m_JAW.Main(ind+1)!=0 ? m_JAW.Main(ind)/m_JAW.Main(ind+1) : 1); }
   double            DiffTeeth(int ind)                { return(m_TEETH.Main(ind+1)!=0 ? m_TEETH.Main(ind)/m_TEETH.Main(ind+1) : 1); }
   double            DiffLips(int ind)                 { return(m_LIPS.Main(ind+1)!=0 ? m_LIPS.Main(ind)/m_LIPS.Main(ind+1) : 1); }

通过下一步, 在 InitIndicators 加入修改, 将我们的指标添加到 EA 函数库中。

bool CSignalKalman::InitIndicators(CIndicators *indicators)
  {
//--- 指标的初始化和附加滤波器的时间序列
   if(!CExpertSignal::InitIndicators(indicators))
      return(false);
//--- 初始化收盘价序列
   if(CheckPointer(m_close)==POINTER_INVALID)
     {
      if(!InitClose(indicators))
         return false;
     }
//--- 创建并初始化 MACD 振荡器
   if(!InitMACD(indicators))
      return(false);
//--- 创建并初始化鳄鱼
   if(!InitAlligator(indicators))
      return(false);
//--- 创建并初始化卡尔曼滤波器
   if(CheckPointer(Kalman)==POINTER_INVALID)
      Kalman=new CKalman(ci_HistoryBars,ci_ShiftPeriod,m_symbol.Name(),ce_Timeframe);
   
//--- ok
   return(true);
  }

然后将其添加到决策函数中。与此同时, 请记住添加的指标应作为滤波器。因此, 只有在接收到主信号后才能对指标进行定位。

int CSignalKalman::LongCondition(void)
  {
   if(!CalculateIndicators())
      return 0;
   int result=0;
   //--- 
   if(cd_correction>cd_forecast)
     {
      if(Signal(1)>Main(1))
         result=80;
      else
        {
         switch(AlligatorTrend(1))
           {
            case 1:
              if(DiffLips(1)>1 && DiffTeeth(1)>1 && DiffJaw(1)<=1)
                 result=80;
              break;
            case -1:
              if(DiffLips(1)>1 || DiffJaw(1)>1)
                 result=80;
              break;
            case 0:
              if(DiffJaw(1)<1)
                {
                 if(DiffLips(1)>1)
                    result=80;
                 else
                    if(DiffTeeth(1)<1)
                       result=80;
                }
              break;
           }
        }
     }
   return result;
  }

类似的修改加入到 ShortCondition 函数中。交易决策模块的完整代码已在附件中提供。

9. 测试加入变更后的 EA

在交易决策模块加入变更后, 创建一个新的 EA (在文章 [5] 中有利用交易信号模块创建 EA 的详述)。我们来测试新创建的 EA, 其参数与本文第二章节的初始测试类似。

正如测试结果所示, 在 EA 参数不变的情况下, 使用滤波器可将盈利因子从 0.75 提升到 1.12。即, 原版 EA 中亏损的参数, 我们却从中获得了盈利。我要提醒一下, 一开始, 我故意采取了原版 EA 的非优化参数。

重复测试重复测试重复测试

结束语

本文演示了一种将交易历史解析到指标的技术, 这就能够建立基于标准指标的过滤系统。通过测试结果, 该系统在实际运行中取得了明显的效益。所建议的系统不仅可用于优化现有的交易系统, 而且可用来创建一个新的系统。

参考资料

  1. 卡尔曼滤波器在价格趋势预测中的应用
  2. HTML 格式的图表和示意图
  3. 一个趋势能够持续多久?
  4. MQL5 中几种确定趋势的方法
  5. 在实践中考察跟踪行情的自适应方法

本文中使用的程序:

#
 名称
类型 
描述 
1 Kalman.mqh  类库  卡尔曼滤波器类
2 SignalKalman.mqh  类库  卡尔曼滤波器的交易信号模块
3 SignalKalman+Filters.mqh  类库  添加指标滤波器后的卡尔曼滤波器交易信号模块
4 Kalman_expert.mq5  智能交易系统  基于卡尔曼滤波器应用策略的原版智能交易系统
5 Kalman+Filters.mq5  智能交易系统  基于卡尔曼滤波器应用策略的修改版智能交易系统
6 Deals_to_Indicators.mq5  智能交易系统  将入场信息解析到指标的智能交易系统
7 Deal.mqh   类库  保存交易信息的类
8 Parsing.mqh  类库  从测试报告中解析交易历史的类
9 Value.mqh   类库  保存指标缓冲区状态数据的类
10 OneBufferArray.mqh  类库  保存单缓冲区指标数据历史的类
11 StaticOneBuffer.mqh  类库  收集和分析单缓冲区指标统计数据的类
    12 ADXValue.mqh  类库  保存 ADX 指标状态数据的类
13 ADX.mqh  类库  保存 ADX 指标数据历史的类
14 StaticADX.mqh  类库  收集和分析 ADX 指标统计数据的类
15 AlligatorValue.mqh  类库  保存鳄鱼指标状态数据的类
16 Alligator.mqh  类库  保存鳄鱼指标数据历史的类
17 StaticAlligator.mqh  类库  收集和分析鳄鱼指标统计数据的类
18 BWMFIValue.mqh  类库  保存比尔·威廉姆斯资金流指数指标状态数据的类
19 BWMFI.mqh  类库  保存比尔·威廉姆斯资金流指数指标数据历史的类
20 StaticBWMFI.mqh  类库  收集和分析单缓冲区比尔·威廉姆斯资金流指数指标统计数据的类
21 MACDValue.mqh  类库  保存 MACD 指标状态数据的类
22 MACD.mqh  类库  保存 MACD 指标数据历史的类
23 StaticMACD.mqh  类库  收集和分析 MACD 指标统计数据的类
24  Reports.zip  存档  存档包含智能系统在策略测试器中的测试结果和分析报表。