English Русский Español Deutsch 日本語 Português
preview
神经网络变得轻松(第十九部分):使用 MQL5 的关联规则

神经网络变得轻松(第十九部分):使用 MQL5 的关联规则

MetaTrader 5交易系统 | 26 九月 2022, 10:56
1 173 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容


概述

在前一篇文章中,我们开始学习隶属无监督学习方法的关联规则挖掘算法。 我们研究了两种算法来解决这类问题:Apriori 和 FP-Growth。 Apriori 算法的瓶颈在于其大量数据库的调用,旨在判定对于频繁形态候选者的支持度。 FP-Growth 方法则通过构建包含整个数据库的树来解决这个问题。 所有更进一步的操作都是依 FP 树执行的,无需访问数据库。 由于 FP 树位于内存当中,这提高了问题解决速度。访问它比完整迭代数据库要快捷得多。


1. 如何在交易中运用该方法

在继续利用 MQL5 构建关联规则挖掘算法之前,我们来研究一下如何在交易中运用它们。

创建关联规则挖掘算法来搜索数据库中二元特征之间的稳定依赖关系。 因此,这些算法可用来寻找各种特征之间的稳定关系。 这可以是由多个指标和/或工具组成的各种形态。 算法不关心每个单独的特征是否代表不同的度量,或者它是不同时间段中相同度量的值。 算法评估时把每个特征均视为独立的。 因此,我们可以尝试将此算法与监督学习方法的开发结合起来。 我们在历史数据的训练样本中添加一些目标特性。 算法应该搜索关联规则,这将导致我们的目标值形成。 

我们已有了一个算法,和如何运用它来解决实际问题的思路。 我们来看看如何利用 MQL5 实现它。 然后我们将在实践中检测这个思路。


2. FP-Growth 算法实现

为了实现上一篇文章中研究的 FP-Growth 算法,我们要记住其构造是基于决策树的。 MQL5 标准库拥有构建二叉树的 CTree 类。 不幸的是,二叉树选项对我们来说并不完全方便,因为一棵 FP 树的一个节点的分支数可以超过 2 个(二元实现中的最大可用分支数)。 因此,在构建算法本身之前,我们先创建 CMyTreeNode 类来实现拥有多个分支的树节点。

2.1. 树的节点类实现

该类将从动态对象数组 CArrayObj 的标准 MQL5 类派生而来。 这个类之所以被选为父类,是因为它拥有与创建和维护动态对象数组相关的所需功能,在我们的例子中,分支节点其实就是动态对象数组。

此外,为了实现算法所需的功能,类中已加入了三个新变量:

  • m_cParent — 指向上一个父节点对象的指针。 对应树根,它将为空
  • m_iIndex — 源数据库中特征的索引;用于识别特征
  • m_dSupport — 接收特征支持度值的变量

class CMyTreeNode : public CArrayObj
  {
protected:
   CMyTreeNode      *m_cParent;
   ulong             m_iIndex;
   double            m_dSupport;

public:
                     CMyTreeNode();
                    ~CMyTreeNode();
  }

在类构造函数中,设置变量的初始值,并清除动态数组。 将类析构函数保留为空。

CMyTreeNode::CMyTreeNode() :  m_iIndex(ULONG_MAX),
                              m_dSupport(0)
  {
   Clear();
  }

为了操控隐藏的类变量,我们将创建一些方法,这些方法将在 FP-Growth 算法的创建过程中用到。 我还会提供方法目的解释及其用途。

class CMyTreeNode : public CArrayObj
  {
   ........
public:
   ........
   //--- methods of access to protected data
   CMyTreeNode*      Parent(void)           const { return(m_cParent); }
   void              Parent(CMyTreeNode *node)  {  m_cParent = node; }
   void              IncreaseSupport(double support)  { m_dSupport += support; }
   double            GetSupport(void)       {  return m_dSupport;  }
   void              SetSupport(double support)       { m_dSupport = support;  }
   ulong             ID(void)             {  return m_iIndex;  }
   void              ID(ulong ID)         {  m_iIndex = ID; }
  };

为了计算置信水平,我们创建 GetConfidence 方法。 在其中,我们首先检查指向前置节点的指针,如果它有效,则将当前节点支持度除以父节点支持度。

注意,FP 树构建算法的组织方式是,任何节点的支持度都不能大于父节点的支持度。 因此,方法操作的结果将始终为正值,并且不会大于 1。

由于树节点是基于现有业务添加的,所以我们没有除零检查。 因此,如果一个节点在树中,那么它的特征在数据库中至少出现过一次,且它的支持度最低。

double CMyTreeNode::GetConfidence(void)
  {
   CMyTreeNode *parent = Parent();
   if(!parent)
      return 1;
//---
   double result = m_dSupport / parent.GetSupport();
   return result;
  }

此外,我们添加了一个创建新分支节点的 AddNode 方法。 在方法参数中,我们传递训练样本源数据库中的特征 ID,和节点的支持度。 该方法返回指向所创建对象的指针。

在方法实体中,我们创建树节点的新实例,并立即检查操作结果。 如果发生错误,则返回无效的对象指针。

接下来,我们指定所创建节点的 ID,并将指向当前对象的指针传递给它,作为父对象。

将新对象添加到当前节点的动态数组中,并检查操作结果。 如果往数组中添加对象时出错,则删除所创建对象,并退出方法,同时返回无效指针。

在方法结束时,将参数中指定的支持度保存到新对象当中,然后退出该方法。

CMyTreeNode *CMyTreeNode::AddNode(const ulong ID, double support = 0)
  {
   CMyTreeNode *node = new CMyTreeNode();
   if(!node)
      return node;
   node.ID(ID);
   if(!Add(node))
     {
      delete node;
      return node;
     }
   node.Parent(GetPointer(this));
//---
   if(support > 0)
      node.SetSupport(support);
   return node;
  }

一旦我们创建了一个新对象,我们应该能够删除它。 父类中已存在依据动态数组中的索引删除对象的方法。 为了扩展功能,我们创建 DeleteNode 方法,并按功能 ID 删除节点。

该方法接收要删除的功能 ID,并返回操作的布尔结果。

在方法实体中,实现循环以在当前节点的动态数组中查找匹配指定 ID 的节点。 循环将迭代遍历从 0 到 m_data_total 变量值范围内的元素。 该变量包含动态数组的活动元素数量,并由父类管控。

在方法实体中,从动态数组中提取下一个元素,并验证指针。 通过调用父类的 Delete 方法(含有指向欲删除元素的索引),可以立即删除含有无效指针的元素。

注意,Delete 方法返回操作的布尔结果。 如果成功地从动态数组中删除了一个元素,则减少循环迭代的计数器,并移动到下一个数组。 我们只减少循环迭代计数器,而不更改 m_data_total 变量值。 这是因为它的值在 Delete 父类的方法中已经更改。

如果在从动态数组中删除无效元素时发生错误,只需移动到数组的下一个元素即可。 我们不会终止方法时返回 false 结果,因为方法任务不是从无效对象中清除动态数组。 这只是一个辅助功能。 该方法的主要任务是删除特定元素。 因此,我们继续执行方法,直至发现所需的元素。

当找到动态数组的所需元素后,调用前面提到的父类的 Delete 方法。 这一次,我们在退出方法的同时返回对象删除结果。

如果迭代遍历动态数组的所有元素后找不到该元素,请采用 false 结果退出该方法。

bool CMyTreeNode::DeleteNode(const ulong ID)
  {
   for(int i = 0; i < m_data_total; i++)
     {
      CMyTreeNode *temp = m_data[i];
      if(!temp)
        {
         if(!Delete(i))
            continue;
         return DeleteNode(ID);
        }
      if(temp.ID() != ID)
         continue;
      return Delete(i);
     }
//---
   return false;
  }

进一步讨论树节点 CMyTreeNode 新类的有关方法,我想提请您注意 Mining 方法。 此方法负责在 FP 树中查找我们正在分析的特征的路径。 在我们继续讲述方法的算法之前,我必须说,它在创建时已考虑到交易中的预期用途。 因此,它稍微偏离了基本算法。

首先,我们不会判定所有特性的关联规则,而只为目标值判定关联规则。 因此,在构建规则树时,我们很可能会遇到这样的情况,即所需的特征不是叶子,而是包含路径中后续元素的节点。 但我们不能忽略后续节点,因为它们会增加目标结果的可能性。 因此,在选择所分析特征的路径时,应考虑它们。

构造此方法时,我曾注意的另一点如下。 根据该算法,我们首先需要在 FP 树中找到所分析特征的所有路径。 然后,我们可以计算选定路径中每个特征的支持度值。 我决定在一个方法中执行这两个子任务。

请注意,为了构建 FP 树,仅计划采用 CMyTreeNode 类实例。 因此,为了执行深度优先搜索,我们将采用递归方法调用。

现在,我们看看在类的 Mining 方法中如何实现这些任务。 在方法参数中,我们传递指向一个向量的指针,该向量是为了写入元素支持度值,一个为了写入路径的矩阵,所分析特征的标识符,以及最小置信级别。 该方法将返回操作的布尔结果。

在方法实体中,首先检查欲分析节点是否是期望的特征。 为此,取节点 ID,并与参数中接收到的期望节点 ID 进行比较。 如果它们相等,则检查当前分支中节点的置信级别。 该级别是调用前面研究的 GetConfidence 方法来判定的。 置信水平不得小于允许的最小值。 否则,以 true 结果退出方法。

bool CMyTreeNode::Mining(vector &supports, matrix &paths, const ulong ID, double min_conf)
  {
   if(ID == m_iIndex)
      if(GetConfidence() < min_conf)
         return true;

下一个模块实现全树的进一步深度搜索。 在此,首先将当前节点的支持度值保存到一个局部变量当中。 然后,运行一个循环,从当前节点到全树深度遍历所有分支。 该方法递归调用搜索所有分支。

注意,采用递归方法,我们只传递期望的标识符,直到找到相应的节点。 此后,我们将 ULONG_MAX 常量传递到全树深度,替代期望的标识符。 这是因为由于 FP 树构造的特殊性,在我们找到期望项的路径之前,形态置信度可能小于 100%。 随着我们沿着这条路径继续前进,期望特征的概率将是 100%。 否则,我们将构建一条不同的路径,绕过期望的节点。

当然,当我们采用自定义算法时,这种情况就被排除在外。 当判定所有特征的规则时,我们开始处理 FP 树中任何节点的时候,它将是一个没有后续节点的叶片。 这是因为所有支持度较低的特征都将被处理,并从树中删除。

因此,当我们偏离算法时,我们必须评估该变化对整个过程的影响,并对算法进行相应调整。 在这种情况下,我们必须将包含期望特征的所有路径添加到列表中。 这是从期望特征到树根的路径,以及从任何后续节点路过期望特征至树根的所有路径。 为此目的,我们需要通知其它节点,在节点和树根之间找到了期望的特征。 当期望特征的 ID 更改为 ULONG_MAX 常量时,会出现此类标志。

在递归方法得到肯定结果之后,对于下一个节点,我们从循环之前创建的局部变量中减去当前节点的支持度值。 如果下一个节点I D 等于期望 ID,则删除已处理的节点。

   double support = m_dSupport;
   for(int i = 0; i < m_data_total; i++)
     {
      CMyTreeNode *temp = m_data[i];
      if(!temp)
        {
         if(Delete(i))
            i--;
         continue;
        }
      if(!temp.Mining(supports, paths, (ID == m_iIndex ? ULONG_MAX : ID), min_conf))
         return false;
      support -= temp.GetSupport();
      if(temp.ID() == ID)
         if(Delete(i))
            i--;
     }

您可看到,在前面的模块中,我们只针对后续节点递归调用了相同的方法,但我们没有保存找到的路径。 保存动作将在下一个方法模块中执行。 对于拥有期望属性的节点和后续节点,将执行该模块。 为此,我们需要检查当前节点的 ID,和参数中接收到的 ID。 此外,递归方法执行后对当前节点的支持度必须大于 “0”。 此外,当前节点不能是根节点。 这意味着它必须至少有一个前置节点。 这是因为我们需要用到一些东西作为规则的先行词。

如果控件成功传递,则将路径矩阵的大小增加 1 行,并在添加的行中填入零值。

接下来,实现从当前节点到树根的扩散循环。 取我们的路径线中当前节点的剩余支持度,分配给当前和所有前置节点。 此外,在相应项的累积支持度向量中添加相同的值。

父迭代完成后,退出方法,并返回肯定结果。

   if(ID == m_iIndex || ID == ULONG_MAX)
      if(support > 0 && !!m_cParent)
        {
         CMyTreeNode *parent = m_cParent;
         ulong row = paths.Rows();
         if(!paths.Resize(row + 1, paths.Cols()))
            return false;
         if(!paths.Row(vector::Zeros(paths.Cols()), row))
            return false;
         supports[m_iIndex] += support;
         while(!!parent)
           {
            if(parent.ID() != ULONG_MAX)
              {
               supports[parent.ID()] += support;
               paths[row, parent.ID()] = support;
              }
            parent = parent.Parent();
           }
        }
//---
   return true;
  }

我用一个小例子解释一下这个方法是如何工作的,因为它的构造稍微超出了上面讲述的 PF-Growth 算法的范围。 假设源数据库内含有以下业务: "AB" 重复两次,一次 "ABC","ABCD" 重复三次,以及一次 "ABCDE". 结果就是,FP 树中形成了以下路径: "A7-B7-C5-D4-E1"。 当分析项目 “C” 时,我们需要从树中恢复包含此项目的所有路径。

我们开始先从根元素 “a” 调用一个方法,并指导它去查找 "C"。 在此我们递归调用节点的方法 "B"。 继续直到叶片 "E"。 鉴于 "E" 叶片没有后续节点,那么从方法的模块 2 开始处理,并写入路径。 此处,我们首先保存 "ABCDE" 路径,并为所有结点写入支持度 1。 这意味着源数据库中有 1 条这样的路径。 然后退出方法,将控制权转交给更高级别。

在 "D" 节点级别保存路径 "ABCD"。 从 "D" 节点的支持度,减去 "E" 叶片的支持度 (4-1=3)。 为这条路径上的所有项,登记该结果值作为支持度。 如您所见,这对应于初始数据,其中我们在训练样本中有 3 笔相同的业务。 我们用项目支持度值,来替代重复业务三次。

同样,保存路径 “ABC”,支持度等于 1。 路径 "AB" 未保存,因为它不包含分析的特征。

在后附的文件 MyTreeNode.mqh 中查找所有类方法的完整代码。

2.2. 关联规则挖掘类的实现

我们继续构建 FP-Growth 关联规则挖掘算法。 主要功能将在另一个类 CAssocRules 中实现。 这个类的结构如下所示。 正如您所看到的,大多数方法都隐藏在“引擎盖下”。 但首要之事依然是首要。

class CAssocRules : public CObject
  {
protected:
   CMyTreeNode       m_cRoot;
   CMyTreeNode       m_cBuyRules;
   CMyTreeNode       m_cSellRules;
   vector            m_vMin;
   vector            m_vStep;
   int               m_iSections;
   matrix            m_mPositions;
   matrix            m_BuyPositions;
   matrix            m_SellPositions;
   //---
   bool              NewPath(CMyTreeNode *root, matrix &path);
   CMyTreeNode      *CheckPath(CMyTreeNode *root, vector &path);
   //---
   bool              PrepareData(matrix &data, matrix &bin_data, 
                                 vector &buy, vector &sell,
                                 const int sections = 10, const double min_sup = 0.03);
   matrix            CreatePath(vector &bin_data, matrix &positions);
   matrix            CreatePositions(vector &support, const double min_sup = 0.03);
   bool              GrowsTree(CMyTreeNode *root, matrix &bin_data, matrix &positions);
   double            Probability(CMyTreeNode *root, vector &data, matrix &positions);

public:
                     CAssocRules();
                    ~CAssocRules();
   //---
   bool              CreateRules(matrix &data, vector &buy, 
                                 vector &sell, int sections = 10, 
                                 double min_freq = 0.03,
                                 double min_prob = 0.3);
   bool              Probability(vector &data, double &buy, double &sell);
   //--- methods for working with files
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual bool      Save(const string file_name);
   virtual bool      Load(const string file_name);
  };

在类变量中,有上述树节点的三个实例:

  • m_cRoot — 我们将用它来记录我们的 FP 树
  • m_cBuyRules — 这个将被用来记录购买规则
  • m_cSellRules  — 这个用于卖出规则

矩阵 m_mPositions、m_BuyPositions 和 m_SellPositions 将包含按降序排序的特征,及其支持度值。

在测试所有之前的模型时,我们检查了在形态第三根蜡烛形成之前判定分形的可能性。 因此,我将依据两个规则挖掘树来定义买入和卖出分形。 如果根据您的问题,您需要为大量的目标特征定义规则,那么您将需要创建更多的专有规则树。

例如,您可能有多个目标买入和卖出级别。 不幸的是,关联规则挖掘算法只依据二元表进行操作。 因此,您必须为每个目标级别创建单独的项,并为其查找关联规则。 您可用动态数组排除大量专有变量。

构造函数和析构函数留空,因为在这个类中,我没有使用指向树构建对象的动态指针。 

如上所述,关联规则挖掘算法仅适用于二元矩阵。 但有关行情状况的数据很难归类。 因此,它们在使用前需要进行预处理。

为了简化类的进一步使用,该算法不需要预先处理来自用户的数据。 取而代之,它是在 PrepareData 方法中实现的。 在参数中,该方法接收指向 2 个矩阵、2 个向量和 2 个常量的指针。 一个矩阵包含原始数据,第二个矩阵用于写入处理后的二元数据。 矢量则包含目标值。 在我们的案例中,它们本就以二元数据表示,因此不需要预处理。 需要对源数据进行预处理。

为了将源数据的标量单位转换为二元值,我们将取每个特征的数值范围,并将其划分为几个区间。 区间数量由 “sections” 参数设置。 每个特征的最小值和步长将保存在相应的矢量 m_vMin 和 m_vStep 当中。 在实际运用过程中,矢量将用于将源数据转换为二元值。

在此我们来准备二元矩阵,设置所需的大小,并以零值填充。 我们还可以指定目标特征的标识符,稍后将其添加到矩阵中的最后一列。

bool CAssocRules::PrepareData(matrix &data,
                              matrix &bin_data,
                              vector &buy, 
                              vector &sell,
                              const int sections = 10,
                              const double min_sup = 0.03)
  {
//---
   m_iSections = sections;
   m_vMin = data.Min(0);
   vector max = data.Max(0);
   vector delt = max - m_vMin;
   m_vStep = delt / sections + 1e-8;
   m_cBuyRules.ID(data.Cols() * m_iSections);
   m_cSellRules.ID(m_cBuyRules.ID() + 1);
   bin_data = matrix::Zeros(data.Rows(), m_cSellRules.ID() + 1);

接下来,执行循环,遍历输入数据矩阵的所有行。 在循环实体中,从每行减去最小值的矢量,然后将结果除以步长。 为了排除超出范围的数据,我们要限制矢量元素的下限和上限值。 作为这些操作的结果,向量的每项中数字的整数部分指示我们需要将源数据的相应项包含到哪个数值范围。 我们的二元矩阵的每个范围都是一个单独的特征。

我们运行一个嵌套循环,并填充二元矩阵的相关行。 如果该特征处于活动状态,则将其值更改为 “1”。 未激活特征则保留零值。

   for(ulong r = 0; r < data.Rows(); r++)
     {
      vector pos = (data.Row(r) - m_vMin) / m_vStep;
      if(!pos.Clip(0, m_iSections - 1))
         return false;
      for(ulong c = 0; c < pos.Size(); c++)
         bin_data[r, c * sections + (int)pos[c]] = 1;
     }
   if(!bin_data.Col(buy, m_cBuyRules.ID()) ||
      !bin_data.Col(sell, m_cSellRules.ID()))
      return false;

填充二元矩阵后,我们可以立即计算每个特征的支持度,并在 CreatePositions 方法中按降序排序。 排序后,以肯定结果退出方法。

   vector supp = bin_data.Sum(0) / bin_data.Rows();
   m_mPositions = CreatePositions(supp, min_sup);
//---
   return true;
  }

既然我们提到了 CreatePositions 特征排序方法,我们就来研究一下它的算法。 该方法在参数中接收支持度向量,和最小支持度级别。

方法实体将包含一些准备工作。 这是因为收到的支持度值由向量表示,其中的项目值包含支持度值。 项目的索引指示特征。 针对矢量项进行简单排序,我们将失去与源数据特征的联系。 因此,我们需要创建“ 特特征 id - 支持度” 数据对。 配对数据将保存到矩阵当中。

为此,首先创建一个标识矩阵,该矩阵有 2 列,行数等于原始样本中的特征数量。 然后按列计算项目的累计和,并将结果矩阵的值减少 “1”。 由此,我们得到一个矩阵,其中的列包含从 “0” 开始的升序值,与行索引对应。 我们只需要用生成的支持度向量替换一列。 因此,我们得到了一个矩阵:每行将包含一个特征标识符,和一个与之对应的支持度值。

matrix CAssocRules::CreatePositions(vector &support, const double min_sup = 0.03)
  {
   matrix result = matrix::Ones(support.Size(), 2);
   result = result.CumSum(0) - 1;
   if(!result.Col(support, 1))
      return matrix::Zeros(0, 0);

我们只需要按支持度降序对矩阵的行进行排序。 为此,采用冒泡排序算法实现一个循环。

   bool change = false;
   do
     {
      change = false;
      ulong total = result.Rows() - 1;
      for(ulong i = 0; i < total; i++)
        {
         if(result[i, 1] >= result[i + 1, 1])
            continue;
         if(result.SwapRows(i, i + 1))
            change = true;
        }
     }
   while(change);

退出循环后,我们将得到一个拥有特征排序的矩阵。 我们只需要从该矩阵中删除不满足最低支持度要求的特征。 为此,找到低于最低支持度级别的第一个特征,并“切断”低于此级别的所有特征。

   int i = 0;
   while(result[i, 1] >= min_sup)
      i++;
   if(!result.Resize(i, 2))
      return matrix::Zeros(0, 0);
//---
   return result;
  }

成功调整矩阵大小之后,退出方法,并返回结果矩阵。

在继续探讨公开方法之前,我们再查看一些方法,其中部分会执行重复功能。 我们需要创建一个路径,从二元数据转换到 FP 树。 此功能将在 CreatePath 方法中执行。 该方法将接收指向二元数据矢量和排序已特征矩阵的指针。 然后,它将返回一个路径矩阵,其中包含要添加到 FP 树中的 “特征 id - 支持度” 对。

请注意已排序特征矩阵之间的差异 ,其一是我们在准备数据时获得的,另一个矩阵是为了添加路径的 FP 树。 这两个矩阵都将包含“特征标识符 - 支持度” 对。 但第一个矩阵包含源数据中提供的所有特征,及其在训练样本中的支持度。 而路径矩阵仅包含所分析业务中存在的特征,以及二元矩阵中表示的来自该业务的支持度。

好了,因为我们处理的是二元矩阵,所以每笔业务中的特征支持度值必须相同。 稍后,我们将采用相同的方法来构建专有规则树。 早前,我们在讲述 CMyTreeNode::Mining 方法时研究过一个示例。 我们在该示例中多次采用支持度级别,取代重复一条路径。 因此,为了统一操作,我们将在 2 个子流程中调用 1 个方法。 在这种情况下,引入支持度级别将非常有用。

在该方法伊始,我们将源数据向量的大小和所分析特征的数量保存在局部变量之中,它小于由支持度低于最小值的随机特征构成的源数据向量大小。

此外,我们还准备了一个矩阵来写出结果。 它不能大于分析特征矩阵。 此外,我们还引入了一个变量来指示路径的尺寸。 在这个阶段,它等于 “0”。

接下来,我们按照支持度的降序循环遍历所有已分析特性。 在循环体中,我们提取下一个选中特征的标识符。 检查二元源数据矢量中的值。 如果该特征未激活,则转到下一个特征。

如果特征处于活动状态,则将二元源数据矢量中的特征 id 及其支持度添加到路径矩阵之中。 之后,我们增加路径尺寸变量。

退出循环后,将路径矩阵的尺寸减至已填充元素的数量,然后退出方法。

matrix CAssocRules::CreatePath(vector &bin_data, matrix &positions)
  {
   ulong size = bin_data.Size();
//---
   ulong total = positions.Rows();
   int vect_pos = 0;
   matrix path = matrix::Zeros(2, total);
   for(ulong c = 0; c < total; c++)
     {
      ulong pos = (ulong)positions[c, 0];
      if(pos >= size)
         continue;
      if(bin_data[pos] == 0)
         continue;
      path[0, vect_pos] = (double)pos;
      path[1, vect_pos] = bin_data[pos];
      vect_pos++;
     }
   if(!path.Resize(2, vect_pos))
      return matrix::Zeros(0, 0);
//---
   return path;
  }

另一种我们需要的方法是将路径添加到 FP 树NewPath。 该方法接收指向树根节点和先前所创建路径矩阵的指针。 然后,它将返回操作的布尔结果。 在方法体中,我们首先检查结果路径的尺寸。 它应该大于 0。 然后,我们增加对根节点的支持度,并运行循环遍历路径的所有项。

在循环体中,检查是否存在含有所需 ID 的下一个节点,如有必要,创建一个新节点。 然后增加节点支持度大小。 然后继续搜索路径中的下一个节点。

遍历路径的所有项后,退出该方法。 

bool CAssocRules::NewPath(CMyTreeNode *root, matrix &path)
  {
   ulong total = path.Cols();
   if(total <= 0)
      return false;
   CMyTreeNode *parent = root;
   root.IncreaseSupport(path[1, 0]);
   for(ulong i = 0; i < total; i++)
     {
      CMyTreeNode *temp = parent.GetNext((ulong)path[0, i]);
      if(!temp)
        {
         temp = parent.AddNode((int)path[0, i], 0);
         if(!temp)
            return false;
        }
      temp.IncreaseSupport(path[1, i]);
      parent = temp;
     }
//---
   return true;
  }

最后,我们继续讨论生成FP 树的方法:GrowsTree。 它在参数中接收指向树根节点的指针、源数据的二元矩阵、和经过排序的已分析特征矩阵。 在方法内部,运行循环遍历源数据里的所有行。

在循环体中,从训练样本中捕获下一笔业务,并调用 CreatePath 方法创建一个路径,添加到树中。 检查并确保接收的部分大于 0。 然后调用 NewPath 方法把所创建路径加进我们的 FP 树。 不要忘记检查操作结果。

成功迭源数据中的所有业务之后,以肯定结果退出该方法,。

bool CAssocRules::GrowsTree(CMyTreeNode * root, matrix & bin_data, matrix &positions)
  {
   ulong rows = bin_data.Rows();
   for(ulong r = 0; r < rows; r++)
     {
      matrix path = CreatePath(bin_data.Row(r), positions);
      ulong size = path.Cols();
      if(size <= 0)
         continue;
      if(!NewPath(root, path))
         return false;
     }
//---
   return true;
  }

现在,我们将上述所有方法合并到公开方法 CreateRules 当中。 在方法参数中,我们传递源数据标量(非二元)的矩阵、目标值的二元向量、把标量值转换为二元值的区间数量、最小支持度和最小置信度。

在方法实体中,我们首先检查接收到的源数据。 我们主要检查获得的矩阵向量的维数与区间数量的对应关系,区间数必须大于 0。

在模块检查之后,将源数据标量转换为二元形似。 这是调用上述 PrepareData 方法完成的。

bool CAssocRules::CreateRules(matrix &data,
                              vector &buy,
                              vector &sell,
                              int sections = 10,
                              double min_sup = 0.03,
                              double min_conf = 0.3)
  {
   if(data.Rows() <= 0 || data.Cols() <= 0 || sections <= 0 ||
      data.Rows() != buy.Size() || data.Rows() != sell.Size())
      return false;
//---
   matrix binary_data;
   if(!PrepareData(data, binary_data, buy, sell, sections))
      return false;

进而,为了移动到相对单位平面,将二元矩阵值除以训练样本中的业务数量,并调用 GrowsTree 方法构建 FP 树。

   double k = 1.0 / (double)(binary_data.Rows());
   if(!GrowsTree(GetPointer(m_cRoot), binary_data * k, m_mPositions))
      return false;

在构建 FP 树之后,我们就能够继续创建规则。 首先,准备一个记录支持度的向量,和一个记录路径的矩阵。 然后调用 Mining 方法,在 FP 树里查找所有含有买入特征的路径。

   vector supports = vector::Zeros(binary_data.Cols());
   binary_data = matrix::Zeros(0, binary_data.Cols());
   if(!m_cRoot.Mining(supports, binary_data, m_cBuyRules.ID(),min_conf))
      return false;

成功提取所有路径后,重置购买特征的支持度,从而将其从所有路径的过程里删除。 并按降序对专有支持度进行排序。 调用 CreatePositions 方法,并把结果记录到 m_BuyPositions 矩阵当中。 如果,特征排序之后,我们仍然能够构建规则(排序矩阵仍然有特征用作规则的先行项),那么调用树成长方法,并将先前选择的路径输入到该方法之中。

作为这些操作的结果,我们将在 m_cBuyRules 节点获得一个含有根的专有规则树。

   supports[m_cBuyRules.ID()] = 0;
   m_BuyPositions = CreatePositions(supports, min_sup);
   if(m_BuyPositions.Rows() > 0)
      if(!GrowsTree(GetPointer(m_cBuyRules), binary_data, m_BuyPositions))
         return false;

与此类似,为卖出特征创建一棵规则树。

   supports = vector::Zeros(binary_data.Cols());
   binary_data = matrix::Zeros(0, binary_data.Cols());
   if(!m_cRoot.Mining(supports, binary_data, m_cSellRules.ID(),min_conf))
      return false;
   supports[m_cSellRules.ID()] = 0;
   m_SellPositions = CreatePositions(supports, min_sup);
   if(m_SellPositions.Rows() > 0)
      if(!GrowsTree(GetPointer(m_cSellRules), binary_data, m_SellPositions))
         return false;
//---
   m_cRoot.Clear();
//---
   return true;
  }

选择所有规则后,清除源 FP 树对象,以便释放计算机资源。 然后以肯定结果退出方法。

“Probability” 方法是为实际应用而创建的。 作为参数,该方法接收源数据的标量向量,和指向两个双精度型变量的指针,这两个变量是为了存储特定的形态置信度。 该方法的算法用到上述所有方法。 您可以在附件中看到它们。

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


3. 测试

我已创建了一个智能系统(assocrules.mq5),并基于实际数据来测试这个类。 EA 的测试完全兼容之前测试中所用的所有参数。 我不能说这个方法能判定所有的分形,且无错误。 但创建的 EA 展现出有趣的结果,如下面的屏幕截图所示。

关联规则类测试结果 


结束语

在本文中,我们研究了采用非监督学习方法解决另一类问题:关联规则挖掘。 我们已创建了一个类,是为了利用 FP-Growth 算法构建关联规则。 以智能系统形式对其进行了测试,结果很有趣。 由此,可以得出结论,这些算法可用来解决交易中的实际问题。

参考文献列表

  1. 神经网络变得轻松(第十四部分):数据聚类
  2. 神经网络变得轻松(第十五部分):利用 MQL5 进行数据聚类
  3. 神经网络变得轻松(第十六部分):聚类运用实践
  4. 神经网络变得轻松(第十七部分):降低维度
  5. 神经网络变得轻松(第十八部分):关联规则

本文中用到的程序

# 名称 类型 说明
1 assocrules.mq5 智能交易系统   训练和测试模型的智能系统
2 AssocRules.mqh 类库
用于组织 FP-Growth 算法的类库
3 MyTreeNode.mqh 类库 组织树节点的类库 


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

附加的文件 |
MQL5.zip (7.78 KB)
CCI 指标。 升级和新特征 CCI 指标。 升级和新特征
在本文中,我将研究升级 CCI 指标的可能性。 此外,我将对指标进行修改。
从头开始开发智能交易系统(第 21 部分):新订单系统 (IV) 从头开始开发智能交易系统(第 21 部分):新订单系统 (IV)
最后,视觉系统将开始工作,尽管它尚未完工。 在此,我们将完成主要更改。 这只是它们当中很少一部份,但都是必要的。 嗯,整个工作将非常有趣。
学习如何基于 Williams PR 设计交易系统 学习如何基于 Williams PR 设计交易系统
本系列中的一篇新文章,介绍了如何依据 MQL5 最流行的技术指标为 MetaTrader 5 设计交易系统。 在本文中,我们将学习如何依据 Williams‘ %R 指标设计交易系统。
DoEasy. 控件 (第 9 部分): 重新编排 WinForms 对象方法、RadioButton 和 Button 控件 DoEasy. 控件 (第 9 部分): 重新编排 WinForms 对象方法、RadioButton 和 Button 控件
在本文中,我将修复 WinForms 对象类方法的名称,并创建 WinForms 的对象 Button 和 RadioButton。