Классы и шаблоны в библиотеках MQL5

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

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

Наиболее простым преобразованием Хафа является выделение на изображении прямых линий путем пересчета в полярные координаты. При такой обработке последовательности "закрашенных" пикселей, выстроенные более или менее в ряд, формируют в пространстве полярных координат пики на пересечении конкретного угла ("тета") наклона прямой и её сдвига ("ро") относительно центра координат.

Преобразование Хафа для прямых линий

Преобразование Хафа для прямых линий

Каждая из трех цветных точек на левом (исходном) изображении оставляет след в пространстве полярных координат (справа), потому что через точку можно провести бесконечное количество прямых линий под разными углами и перпендикулярами до центра. Каждый фрагмент следа "отмечается" только один раз, за исключением красной метки: в этом месте все три следа пересекаются и дают максимальный отклик (3). И действительно, как мы видим на исходном изображении, есть прямая линия, которая проходит через все три точки. Таким образом, два параметра прямой и выявляются максимумом в полярных координатах.

Мы можем использовать такое преобразование Хафа на графиках котировок для выделения альтернативных линий поддержки и сопротивления. Если обычно такие линии проводятся по отдельным экстремумам и, фактически, производят анализ выбросов, то линии преобразования Хафа могут учитывать все цены High или все цены Low, или даже распределение тиковых объемов внутри баров: все это позволяет получить более обоснованную оценку уровней.

Проработку начнем с заголовочного файла LibHoughTransform.mqh. Поскольку исходные данные для анализа поставляет некое абстрактное изображение, определим шаблон интерфейса HoughImage.

template<typename T>
interface HoughImage
{
   virtual int getWidth() const;
   virtual int getHeight() const;
   virtual T get(int xint yconst;
};

Все, что нужно знать об изображении при его обработке, это его размеры и содержимое каждого пикселя, которое из соображений общности представлено параметрическим типом T. Понятно, что в простейшем случае это может быть int или double.

С вызовом аналитической обработки изображения все немного сложнее: для неё требуется описать в библиотеке класс, объекты которого мы станем возвращать из специальной фабричной функции (в виде указателей), и именно эта функция должна экспортироваться из библиотеки. Предположим, что так:

template<typename T>
class HoughTransformDraft
{
public:
   virtual int transform(const HoughImage<T> &imagedouble &result[],
      const int elements = 8) = 0;
};
   
HoughTransformDraft<?> *createHoughTransform() export { ... } // Проблема - шаблон!

Однако шаблонные типы и шаблонные функции нельзя экспортировать. Поэтому мы сделаем промежуточный нешаблонный класс HoughTransform, а в нем — шаблонный метод под параметр изображения. К сожалению, шаблонные методы не могут быть виртуальными, и потому выполним внутри метода диспетчеризацию вызовов вручную (с помощью dynamic_cast), переадресовывая обработку классу-наследнику с виртуальным методом.

class HoughTransform
{
public:
   template<typename T>
   int transform(const HoughImage<T> &imagedouble &result[],
      const int elements = 8)
   {
      HoughTransformConcrete<T> *ptr = dynamic_cast<HoughTransformConcrete<T> *>(&this);
      if(ptrreturn ptr.extract(imageresultelements);
      return 0;
   }
};
   
template<typename T>
class HoughTransformConcretepublic HoughTransform
{
public:
   virtual int extract(const HoughImage<T> &imagedouble &result[],
      const int elements = 8) = 0;
};

Внутреннюю реализацию класса HoughTransformConcrete напишем в файле библиотеки MQL5/Libraries/MQL5Book/LibHoughTransform.mq5.

#property library
   
#include <MQL5Book/LibHoughTransform.mqh>
   
template<typename T>
class LinearHoughTransformpublic HoughTransformConcrete<T>
{
protected:
   int size;
   
public:
   LinearHoughTransform(const int quants): size(quants) { }
   ...

Поскольку мы собираемся пересчитывать точки изображения в пространство в новых — полярных — координатах, следует выделить под задачу некоторый размер. Уточним, что речь идет о дискретном преобразовании Хафа, поскольку мы и исходное изображение рассматриваем как дискретный набор точек (пикселей), и значения углов с перпендикулярами станем аккумулировать ячейками (квантами). Для простоты остановимся на варианте с квадратным пространством, где количество отсчетов и по углу, и по расстоянию до центра, равно. Этот параметр передается в конструктор класса.

template<typename T>
class LinearHoughTransformpublic HoughTransformConcrete<T>
{
protected:
   int size;
   Plain2DArray<Tdata;
   Plain2DArray<doubletrigonometric;
   
   void init()
   {
      data.allocate(sizesize);
      trigonometric.allocate(2size);
      double td = M_PI / size;
      int i;
      for(i = 0t = 0i < sizei++, t += d)
      {
         trigonometric.set(0iMathCos(t));
         trigonometric.set(1iMathSin(t));
      }
   }
   
public:
   LinearHoughTransform(const int quants): size(quants)
   {
      init();
   }
   ...

Для подсчета статистики "следов", оставляемых "закрашенными" пикселями в преобразованном пространстве размера size на size, описан массив data. Для него использован вспомогательный класс-шаблон Plain2DArray (с параметром-типом T), позволяющий эмулировать двумерный массив произвольных размеров. Тот же класс, но с параметром-типом double, применен для таблицы предварительно рассчитанных значений синусов и косинусов углов trigonometric: она потребуется для быстрого отображения пикселей в новое пространство.

Метод детектирования параметров наиболее заметных прямых линий называется extract. Он принимает на вход изображение и должен заполнить выходной массив result найденными парами параметров прямых линий. В уравнении:

y = a * x + b

параметр a (наклон, "тета") будет записываться по четным номерам массива result, а параметр b (отступ, "ро") — по нечетным. Например, первая, наиболее заметная, прямая после завершения работы метода описывается выражением:

y = result[0] * x + result[1];

Для второй прямой индексы увеличатся до 2 и 3, соответственно, и так далее, вплоть до максимально запрошенного количества линий (lines). Размер массива result равен удвоенному количеству прямых.

template<typename T>
class LinearHoughTransformpublic HoughTransformConcrete<T>
{
   ...
   virtual int extract(const HoughImage<T> &imagedouble &result[],
      const int lines = 8override
   {
      ArrayResize(resultlines * 2);
      ArrayInitialize(result0);
      data.zero();
   
      const int w = image.getWidth();
      const int h = image.getHeight();
      const double d = M_PI / size;     // 180 / 36 = 5 градусов, например
      const double rstep = MathSqrt(w * w + h * h) / size;
      ...

В блоке поиска прямых линий организованы вложенные циклы по пикселям изображения. Для каждой "закрашенной" (ненулевой) точки производится цикл по углам наклона, и соответствующие пары полярных координат помечаются в преобразованном пространстве. В данном случае мы просто вызываем метод увеличения содержимого ячейки на то значение, что вернул пиксель — data.inc((int)r, i, v), но в зависимости от прикладной задачи и типа T может потребовать и более сложная обработка.

      double rt;
      int i;
      for(int x = 0x < wx++)
      {
         for(int y = 0y < hy++)
         {
            T v = image.get(xy);
            if(v == (T)0continue;
   
            for(i = 0t = 0i < sizei++, t += d// t < Math.PI
            {
               r = (x * trigonometric.get(0i) + y * trigonometric.get(1i));
               r = MathRound(r / rstep); // диапазон [-size, +size]
               r += size// [0, +2size]
               r /= 2;
   
               if((int)r < 0r = 0;
               if((int)r >= sizer = size - 1;
               if(i < 0i = 0;
               if(i >= sizei = size - 1;
   
               data.inc((int)riv);
            }
         }
      }
      ...

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

      for(i = 0i < linesi++)
      {
         int xy;
         if(!findMax(xy))
         {
            return i;
         }
   
         double a = 0b = 0;
         if(MathSin(y * d) != 0)
         {
            a = -1.0 * MathCos(y * d) / MathSin(y * d);
            b = (x * 2 - size) * rstep / MathSin(y * d);
         }
         if(fabs(a) < DBL_EPSILON && fabs(b) < DBL_EPSILON)
         {
            i--;
            continue;
         }
         result[i * 2 + 0] = a;
         result[i * 2 + 1] = b;
      }
   
      return i;
   }

Использованный здесь вспомогательный метод findMax (см. исходный код) записывает в переменные x и y координаты максимального значения в новом пространстве, дополнительно затирая окрестность этого места, чтобы не находить его снова и снова.

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

HoughTransform *createHoughTransform(const int quants,
   const ENUM_DATATYPE type = TYPE_INTexport
{
   switch(type)
   {
   case TYPE_INT:
      return new LinearHoughTransform<int>(quants);
   case TYPE_DOUBLE:
      return new LinearHoughTransform<double>(quants);
   ...
   }
   return NULL;
}

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

Чтобы проверить экспорт/импорт структур мы также описали структуру с мета-информацией о преобразовании в данной версии библиотеки и экспортировали функцию, возвращающую такую структуру.

struct HoughInfo
{
   const int dimension// количество параметров в формуле модели
   const string about;  // словесное описание
   HoughInfo(const int nconst string s): dimension(n), about(s) { }
   HoughInfo(const HoughInfo &other): dimension(other.dimension), about(other.about) { }
};
   
HoughInfo getHoughInfo() export
{
   return HoughInfo(2"Line: y = a * x + b; a = p[0]; b = p[1];");
}

Дело в том, что различные модификации преобразований Хафа могут выявлять не только прямые линий, но и другие построения, отвечающие заданной аналитической формуле (например, окружности). Такие модификации будут выявлять другое количество параметров и нести другой смысл. Наличие самодокументирующей функции способно упростить интеграцию библиотек (особенно, когда их становится много; заметьте, что наш заголовочный файл содержит только информацию общего плана, относящуюся к любой библиотеке, реализующей данный интерфейс преобразования Хафа, а не только для прямых линий).

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

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

Исходный код индикатора находится в файле MQL5/Indicators/MQL5Book/p7/­LibHoughChannel.mq5. Он также подключает заголовочный файл LibHoughTransform.mqh, куда мы добавили директиву импорта.

#import "MQL5Book/LibHoughTransform.ex5"
HoughTransform *createHoughTransform(const int quants,
   const ENUM_DATATYPE type = TYPE_INT);
HoughInfo getHoughInfo();
#import

В анализируемом изображении обозначим пикселями положение специфических типов цен (OHLC) в котировках. Для реализации изображения потребуется описать класс HoughQuotes, производный от HoughImage<int>.

Предусмотрим "закрашивание" пикселей несколькими способами: внутри тела свечей, внутри полного размаха свечей, а также непосредственно в максимумах и минимумах. Всё это формализовано в перечислении PRICE_LINE. Пока в индикаторе будут использованы только HighHigh и LowLow, но это можно вынести в настройки.

class HoughQuotespublic HoughImage<int>
{
public:
   enum PRICE_LINE
   {
      HighLow = 0,   // Bar Range |High..Low|
      OpenClose = 1// Bar Body |Open..Close|
      LowLow = 2,    // Bar Lows
      HighHigh = 3,  // Bar Highs
   };
   ...

В параметрах конструктора и внутренних переменных укажем диапазон баров для анализа. Количество баров size определяет размер изображение по горизонтали. Для простоты возьмем такое же количество отсчетов и по вертикали. Поэтому шаг дискретизации цен (step) равен фактическому размаху цен (pp) за эти size баров, деленному на size. Для переменной base вычислим нижнюю границу цен, которые подпадают под рассмотрение в указанных барах. Эта переменная потребуется для привязки построения линий на основе найденных параметров преобразования Хафа.

protected:
   int size;
   int offset;
   int step;
   double base;
   PRICE_LINE type;
   
public:
   HoughQuotes(int startbarint barcountPRICE_LINE price)
   {
      offset = startbar;
      size = barcount;
      type = price;
      int hh = iHighest(NULL0MODE_HIGHsizestartbar);
      int ll = iLowest(NULL0MODE_LOWsizestartbar);
      int pp = (int)((iHigh(NULL0hh) - iLow(NULL0ll)) / _Point);
      step = pp / size;
      base = iLow(NULL0ll);
   }
   ...

Напомним, что интерфейс HoughImage обязывает нас реализовать 3 метода: getWidth, getHeight и get. С первыми двумя все просто.

   virtual int getWidth() const override
   {
      return size;
   }
   
   virtual int getHeight() const override
   {
      return size;
   }

Метод get для получения "пикселей" на основе котировок возвращает 1, если указанная точка попадает внутрь диапазона бара или ячейки, согласно выбранному методу расчета из PRICE_LINE. В противном случае возвращается 0. Данный метод можно существенно усовершенствовать, если оценивать фракталы, последовательно увеличивающиеся экстремумы или "круглые" цены с более высоким весом (жирностью пикселей).

   virtual int get(int xint yconst override
   {
      if(offset + x >= iBars(NULL0)) return 0;
   
      const double price = convert(y);
      if(type == HighLow)
      {
         if(price >= iLow(NULL0offset + x) && price <= iHigh(NULL0offset + x))
         {
            return 1;
         }
      }
      else if(type == OpenClose)
      {
         if(price >= fmin(iOpen(NULL0offset + x), iClose(NULL0offset + x))
         && price <= fmax(iOpen(NULL0offset + x), iClose(NULL0offset + x)))
         {
            return 1;
         }
      }
      else if(type == LowLow)
      {
         if(iLow(NULL0offset + x) >= price - step * _Point / 2
         && iLow(NULL0offset + x) <= price + step * _Point / 2)
         {
            return 1;
         }
      }
      else if(type == HighHigh)
      {
         if(iHigh(NULL0offset + x) >= price - step * _Point / 2
         && iHigh(NULL0offset + x) <= price + step * _Point / 2)
         {
            return 1;
         }
      }
      return 0;
   }

Вспомогательный метод convert обеспечивает пересчет из пиксельных y-координат в ценовые значения.

   double convert(const double yconst
   {
      return base + y * step * _Point;
   }
};

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

input int BarOffset = 0;
input int BarCount = 21;
input int MaxLines = 3;
   
const string Prefix = "HoughChannel-";

Объект, предоставляющий сервис по преобразованию, опишем как глобальный: именно здесь вызывается фабричная функция createHoughTransform из библиотеки.

HoughTransform *ht = createHoughTransform(BarCount);

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

int OnInit()
{
   HoughInfo info = getHoughInfo();
   Print(info.dimension" per "info.about);
   return INIT_SUCCEEDED;
}

Расчет в OnCalculate будем выполнять однократно на открытии бара.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   static datetime now = 0;
   if(now != iTime(NULL00))
   {
      ... // см. следующий блок
      now = iTime(NULL00);
   }
   return rates_total;
}

Сам расчет преобразования запускается дважды на паре изображений (highs и lows), сформированных по разным типам цен. При этом работу последовательно выполняет один и тот же объект ht. Если выявление прямых линий завершилось успешно, отображаем их на графике с помощью функции DrawLine. Поскольку линии перечислены в массиве результатов в порядке убывания важности, линиям назначается убывающая жирность.

      HoughQuotes highs(BarOffsetBarCountHoughQuotes::HighHigh);
      HoughQuotes lows(BarOffsetBarCountHoughQuotes::LowLow);
      static double result[];
      int n;
      n = ht.transform(highsresultfmin(MaxLines5));
      if(n)
      {
         for(int i = 0i < n; ++i)
         {
            DrawLine(highsPrefix + "Highs-" + (string)i,
               result[i * 2 + 0], result[i * 2 + 1], clrBlue5 - i);
         }
      }
      n = ht.transform(lowsresultfmin(MaxLines5));
      if(n)
      {
         for(int i = 0i < n; ++i)
         {
            DrawLine(lowsPrefix + "Lows-" + (string)i,
               result[i * 2 + 0], result[i * 2 + 1], clrRed5 - i);
         }
      }

Функция DrawLine основана на трендовых графических объектах (OBJ_TREND, см. исходный код).

При деинициализации индикатора удаляем линии и аналитический объект.

void OnDeinit(const int)
{
   AutoPtr<HoughTransformdestructor(ht);
   ObjectsDeleteAll(0Prefix);
}

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

Запуск индикатора с настройками по умолчанию дает примерно такую картину.

Индикатор с основными линиями по ценам High/Low на базе библиотеки с преобразованием Хафа

Индикатор с основными линиями по ценам High/Low на базе библиотеки с преобразованием Хафа

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

Предусмотрим макрос LIB_HOUGH_IMPL_DEBUG для включения внедрения исходника библиотеки непосредственно в индикатор. Макрос следует размещать перед включение заголовочного файла.

#define LIB_HOUGH_IMPL_DEBUG
#include <MQL5Book/LibHoughTransform.mqh>

В самом заголовочном файле обложим блок импорта из двоичной обособленной копии библиотеки инструкциями условной компиляции препроцессора. Когда макрос включен, будет работать другая ветка, с инструкцией #include.

#ifdef LIB_HOUGH_IMPL_DEBUG
#include "../../Libraries/MQL5Book/LibHoughTransform.mq5"
#else
#import "MQL5Book/LibHoughTransform.ex5"
HoughTransform *createHoughTransform(const int quants,
   const ENUM_DATATYPE type = TYPE_INT);
HoughInfo getHoughInfo();
#import
#endif

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

HoughInfo getHoughInfo() export
{
#ifdef LIB_HOUGH_IMPL_DEBUG
   Print("inline library (debug)");
#else
   Print("standalone library (production)");
#endif
   return HoughInfo(2"Line: y = a * x + b; a = p[0]; b = p[1];");
}

Если в коде индикатора, в файле LibHoughChannel.mq5 раскомментировать инструкцию #define LIB_HOUGH_IMPL_DEBUG, вы можете попробовать пошагово выполнить анализ изображений.