English 中文 Español Deutsch 日本語 Português
preview
Нейросети — это просто (Часть 19): Ассоциативные правила средствами MQL5

Нейросети — это просто (Часть 19): Ассоциативные правила средствами MQL5

MetaTrader 5Торговые системы | 4 июля 2022, 14:50
902 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Содержание


Введение

В предыдущей статье мы начали знакомиться с алгоритмами поиска ассоциативных правил, которые относятся к методам обучения без учителя. Мы с вами рассмотрели 2 алгоритма для решения данного типа задач: Apriori и FP-Growth. Узким местом алгоритма Apriori является большое количество обращений к базе данных для определения поддержек кандидатов в частые паттерны. В методе FP-Growth это решается путем построения дерева, в которое сжимается вся база данных. И вся последующая работа осуществляется с FP-деревом, без обращений к базе данных. Что позволяет повысить скорость решения задачи. Так как FP-дерево помещается в оперативной памяти. И доступ к нему значительно быстрее полного перебора базы данных.


1. Вариант использованию метода в трейдинге

Прежде чем перейти к построению алгоритма поиска ассоциативных правил средствами MQL5 давайте подумаем, каким образом мы можем их использовать в своей торговле.

Алгоритмы поиска ассоциативных правил были созданы для поиска устойчивых зависимостей между бинарными признаками в базе данных. Следовательно, с помощью подобных алгоритмов мы можем найти некие устойчивые связи между различными признаками. Это могут быть различные паттерны, состоящие из нескольких индикаторов и/или инструментов. При этом, алгоритму абсолютно все равно каждый отдельный признак представляет собой различные показатели или это значения одного показателя в различных временных интервалах. Алгоритм оценивает каждый признак как не зависимый. Таким образом, почему бы нам не попробовать скрестить данный алгоритм с наработками методов обучения с учителем. Давайте возьмем и добавим к обучающей выборке исторических данных несколько целевых признаков. И попросим алгоритм найти нам ассоциативные правила, вследствие которых будут формироваться наши целевые значения. 

У нас есть алгоритм и идея его использования для решения своих практических задач. Давайте посмотрим на возможность его реализации средствами MQL5. И затем проверим на практике нашу идею.


2. Реализация алгоритма FP-Growth

Приступая к реализации рассмотренного в предыдущей статье алгоритма FP-Growth давайте вспомним, что в основе его построения лежит дерево решений. В стандартной библиотеке MQL5 представлен класс CTree для построения бинарного дерева. К сожалению, нам не совсем удобен вариант бинарного дерева, так как в процессе построения FP-дерева количество ответвлений от одного узла может быть больше 2-х (предусмотренных бинарной реализацией). Поэтому, перед построением самого алгоритма, мы создадим класс CMyTreeNode для реализации узла дерева с несколькими ответвлениями.

2.1. Реализация класса узла дерева

Данный класс мы создадим наследником от стандартного класса динамического массива объектов в MQL5 CArrayObj. Выбор этого класса в качестве родительского обусловлен наличием функционала по созданию и обслуживанию динамического массива объектов, которыми и будут являться наши узлы ответвлений.

Дополнительно, для реализации функционала, необходимого в рамках выстраиваемого алгоритма, в новый класс мы добавили 3 переменных:

  • 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".

Мы также не осуществляем проверку деления на ноль, так как узлы в дерево добавляются из существующих транзакций. Следовательно, если узел есть в дереве, значит его признак как минимум 1 раз появился в базе данных и имеет минимальную поддержку.

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

Также мы добавим метод создания нового узла ответвления AddNode. В параметрах метода мы передадим идентификатор признака в исходной базе данных обучающей выборки и поддержку создаваемого узла. Вернет наш метод указатель на созданный объект.

В теле метода мы создаем новый экземпляр узла нашего дерева и сразу проверяем результат выполнения операции. При возникновении ошибки вернем не действительный указатель объекта.

Далее мы указываем идентификатор созданного узла и передадим ему указатель на текущий объект в качестве родительского объекта.

Добавим новый объект в динамический массив текущего узла и проверим результат операции. В случае ошибки добавления объекта в массив удалим созданный объект и выходим из метода, вернув не действительный указатель.

В заключении метода сохраним указанную в параметрах поддержку в новом объекте и выходим из метода.

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.

В параметрах методу мы передадим идентификатор удаляемого признака, а вернем логический результат операции.

В теле метода мы организуем цикл по поиску узла с заданным идентификатором в динамическом массиве текущего узла. Данный цикл будет перебирать элементы в диапазоне от 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-дереве все пути к анализируемому признаку. А затем посчитать поддержки для каждого признака в выбранных путях. Я принял решении выполнить 2 эти подзадачи в рамках одного метода.

Сразу надо сказать, что для построения FP-дерева мы планируем использовать только экземпляры класс CMyTreeNode. Следовательно, для осуществления поиска в глубину мы воспользуемся рекурсивным вызовом рассматриваемого метода.

А теперь давайте посмотрим на реализацию указанных задач в методе Mining нашего класса. В параметрах методу мы передадим указатели на вектор для записи поддержек элементов, матрицу для записи путей, идентификатор анализируемого признака и уровень минимального доверия. Возвращать наш метод будет логическое значение выполнения операции.

В теле метода мы сначала проверим является ли анализируемый узел искомым признаком. Для этого мы сравним идентификатор нашего узла и полученный в параметрах идентификатор искомого узла. При равенстве идентификаторов мы проверяем степень доверия узла в текущей ветке, которую мы определяем с помощью выше рассмотренного метода 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-дереве оно будет листом без последующих узлов. Так как все признаки с меньшей поддержкой будут уже проработаны и удалены из дерева.

Таким образом, при любом отступлении от алгоритма мы должны оценить влияние вносимых изменений на весь процесс и внести соответствующие коррективы алгоритма. В данном случае мы должны будем внести в список все пути, включающие искомый признак. А это путь от искомого признака до корня дерева и все варианты путей от любого из последующего узла до корня дерева, проходящие через искомый признак. А для этого мы должны указать последующим узлам, что искомый признак был уже найден между ними и корнем дерева. Таким сигнальным флагом и будет изменение идентификатора искомого признака на константу ULONG_MAX.

После положительного результата отработки рекурсивного метода для последующего узла мы отнимаем значение поддержки от созданной перед циклом локального переменной с поддержкой текущего узла. И если идентификатор последующего узла равен искомому, то мы удаляем отработанный узел.

   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--;
     }

Можно заметить, что в предыдущем блоке мы только вызывали рекурсивно этот же метод для последующих узлов, но не сохраняли найденные пути. Этот функционал мы будем выполнять в следующем блоке метода. Данный блок будет выполняться для узлов с искомым признаком и последующих. Для этого мы организуем проверку идентификатора текущего узла и полученного в параметрах. Кроме того, поддержка текущего узла, оставшаяся после отработки рекурсивных методов, должна быть больше "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;
  }

Наверное, стоит пояснить работу метода на небольшом примере. Ведь его построение немного выходит за рамки описанного ранее алгоритма FP-Growth. Предположим, в исходной базе у нас 2 раза повторилась транзакция "AB", 1 раз — "ABC", 3 раза — "ABCD" и 1 раз — "ABCDE". В результате в FP-дереве был сформирован путь "A7-B7-C5-D4-E1". При анализе признака "C" нам нужно восстановить из дерева все пути, содержащие данный признак.

Начинаем мы работу с вызова метода для корневого элемента "А" и передаем ему указание найти признак "С". В нем мы рекурсивно вызываем метод для узла "В". И так по цепочке до листа "E". Так как у листа "E" нет последующих узлов, мы начинаем отработку 2-го блока нашего метода и записываем пути. Здесь мы сначала сохраним путь "ABCDE" и запишем всем узлам поддержку 1. Это означает, что такой путь в исходной базе был 1 раз. и выйдем из метода, передав управление на уровень выше.

На уровне узла "D" мы сохраним путь "ABCD". При этом от поддержки узла "D" вычтем поддержку листа "E" (4-1=3) проставим полученное значение в качестве поддержек всех элементов данного пути. Как можно заметить, это соответствует исходным данным о существовании 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);
  };

Среди переменных класса можно заметить 3 экземпляра выше описанных узлов дерева:

  • m_cRoot — будем использовать для записи нашего FP-дерева;
  • m_cBuyRules — для записи правил на покупку;
  • m_cSellRules  — для записи правил на продажу.

А матрицы m_mPositions, m_BuyPositions и m_SellPositions будут содержать признаки и их поддержки, отсортированные в порядке убывания.

Напомню, что при тестировании всех предыдущих моделей мы проверяли возможность определения фрактала до формирования 3-й свечи паттерна. Поэтому я буду определять 2 дерева правил поиска для определения фракталов на покупку и продажу. Если требованиями Вашей задачи предполагается определения правил для большего количества целевых признаков, то Вам будет необходимо создать и больше частных деревьев правил.

К примеру, у Вас может быть несколько целевых уровней на покупку и несколько на продажу. К сожалению, алгоритмы поиска ассоциативных правил оперируют только с бинарными таблицами. Следовательно, для каждого целевого уровня Вам придется создать отдельный признак и найти для него ассоциативные правила. Для исключения большого числа частных переменных вы можете воспользоваться динамическими массивами.

Конструктор и деструктор метода я оставил пустыми, так как в данном классе я не использовал динамические указатели на объекты построения деревьев. 

Выше мы уже упомянули, что алгоритмы поиска ассоциативных правил работают только с бинарными матрицами. А данные о состоянии рыночной ситуации сложно отнести к таковым. Поэтому перед их использованием потребуется предварительная обработка.

Для упрощения последующего использования класса я не стал требовать предварительную обработку данных от пользователя, а организовал её в методе 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, предлагаю рассмотреть сразу и его алгоритм. В параметрах метод получает вектор поддержек и уровень минимальной поддержки.

В теле метода мы проведем небольшую подготовительную работу. Дело в том, что в полученные поддержки представлены вектором. Значения элементов которых содержат поддержки. А порядковые номера элементов указывают на признаки. При простой сортировке элементов вектора мы потеряем связь с признаками исходных данных. Поэтому нам необходимо создать пары "идентификатор признака — поддержка". И данные пары мы сохраним в матрицу.

Для этого мы сначала создадим единичную матрицу из 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-дерево.

Следует сразу обратить внимание на разницу между отсортированной матрицей признаков, которую мы получили при подготовке данных, и матрицей для добавления пути в FP-дерево. Обе матрицы будут содержать пары "идентификатор признака - поддержка". Только первая матрица содержит все признаки, имеющиеся в исходных данных и их поддержки в обучающей выборке. Вторая же матрица (пути) будет содержать только признаки, присутствующие в анализируемой транзакции, и поддержки из этой транзакции, которые указаны в бинарной матрице.

Да, мы имеем дело с бинарной матрицей и поддержки признаков в каждой транзакции должны быть одинаковыми. Но позже мы воспользуемся этим же методом для построения частных деревьев правил. Выше мы приводили пример при описании метода CMyTreeNode::Mining. И там мы вместо повторения одного пути несколько раз использовали уровни поддержки. Именно для унификации операций мы будем использовать 1 метод в 2-х подпроцессах. И тогда введение уровня поддержек нам будет очень полезно.

В начале метода мы сохраним в локальные переменные размеры вектора исходных данных и количество анализируемых признаков, которое будет меньше размера вектора исходных данных на количество случайных признаков с поддержкой ниже минимальной.

Тут же мы подготовим матрицу для записи результатов, которая не может быть больше матрицы анализируемых признаков. И введем переменную, указывающую размер нашего пути. На данном этапе он равен "0".

Далее мы организуем цикл, в котором будем перебирать все анализируемые признаки в порядке убывания их поддержек. В теле цикла мы извлекаем идентификатор очередного проверяемого признака. И проверим его значение в бинарном векторе исходных данных. Если признак не активен, то переходим к следующему признаку.

Если же признак активен, то мы добавляем в матрицу пути идентификатор признака и его поддержку из бинарного вектора исходных данных. И тут же увеличиваем переменную размера пути.

После выхода из цикла мы уменьшаем размер матрицы пути до количества заполненных элементов и выходим из метода.

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". Далее мы увеличиваем поддержку корневого узла и начинаем цикл перебора всех элементов пути.

В теле цикла мы проверяем наличие следующего узла с требуемым идентификаторов и, при необходимости создаем новый узел. После чего увеличиваем размер поддержки узла. И переходим к поиску следующего узла в пути.

По завершению перебора всех элементов пути выходим из метода. 

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;

Далее, чтобы перейти в плоскость относительных единиц мы разделим значения нашей бинарной матрицы на количество транзакций в обучающей выборке и построим наше FP-дерево с помощью метода GrowsTree.

   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. В параметрах метод получает скалярный вектор исходных данных и указатели на 2 переменные типа double, в которые будут записаны степени доверия тому или иному паттерну. В алгоритме метода используются все рассмотренные выше методы. И вы можете с ним ознакомиться во вложении.

Полный код всех классов и их методов приведен во вложении.


3. Тестирование

Для проверки работы построенного нами класса на реальных данных мы создали советник "assocrules.mq5". Надо сказать, что тестирование советника осуществлялось с полным соблюдением всех параметров, используемых при предыдущих тестах. Не могу сказать, что метод определил все фракталы без ошибок. Но созданный советник продемонстрировал интересные результаты, которые приведены на скриншоте ниже.

Результаты тестирования класса ассоциативных правил 


Заключение

В данной статье мы рассмотрели еще один тип задач, решаемых методами обучения без учителя — поиск ассоциативных правил. Мы создали класс для создания ассоциативных правил по алгоритму FP-Growth. Для тестирования метода был создан советник, который показал неплохие результаты. В результате мы можем сделать вывод о возможности использования подобных алгоритмов для решения практических задач в трейдинге.

Ссылки

  1. Нейросети — это просто (Часть 14): Кластеризация данных
  2. Нейросети — это просто (Часть 15): Кластеризации данных средствами MQL5
  3. Нейросети — это просто (Часть 16): Практическое использование кластеризации
  4. Нейросети — это просто (Часть 17): Понижение размерности
  5. Нейросети — это просто (Часть 18): Ассоциативные правила

Программы, используемые в статье

# Имя Тип Описание
1 assocrules.mq5 Советник   Советник для обучения и тестирования модели
2 AssocRules.mqh Библиотека класса
Библиотека класса для организации алгоритма FP-Growth
3 MyTreeNode.mqh Библиотека класса Библиотека класса организации узла дерева 


Прикрепленные файлы |
MQL5.zip (7.78 KB)
Индикатор CCI. Модернизация и новые возможности Индикатор CCI. Модернизация и новые возможности
В этой статье мы рассмотрим возможность модернизации индикатора CCI. Кроме того, будет представлен пример модификации этого индикатора.
Видео: Простая автоматизированная торговля – Как создать простой торговый советник с помощью MQL5 Видео: Простая автоматизированная торговля – Как создать простой торговый советник с помощью MQL5
Большинство слушателей моих курсов считали, что язык MQL5 сложен для понимания. Кроме того, они искали простые способы автоматизации некоторых процессов. В этой статье вы узнаете как сходу начать работать в MQL5 даже без навыков программирования и даже если в прошлом у вас уже были неудачные попытки освоить эту тему.
Разработка торгового советника с нуля (Часть 14): Добавляем Volume at Price (II) Разработка торгового советника с нуля (Часть 14): Добавляем Volume at Price (II)
Сегодня мы добавим несколько ресурсов в наш советник. Эта интересная статья может натолкнуть вас на новые идеи и методы представления информации и в то же время исправить мелкие недочеты в ваших проектах.
Разработка торговой системы на основе индикатора MACD Разработка торговой системы на основе индикатора MACD
В этой статье мы познакомимся с очередным инструментом из нашей серии: мы узнаем, как создать торговую систему на основе одного из самых популярных технических индикаторов — Moving Average Convergence Divergence (MACD).