MQL5 库中的类和模板

尽管通常禁止对类和模板进行导出和导入,但开发人员可以通过将抽象基接口的说明移动到库头文件中并传递指针来绕过这些限制。我们用一个库的例子(执行图像霍夫变换)来说明这个概念。

霍夫变换是一种提取图像特征的算法,该算法通过将图像与由一组参数说明的某种形式模型(公式)进行比较。

最简单的霍夫变换是通过将图像上的直线转换成极坐标来选择直线。通过这种处理,排列成一行的“填充”像素序列在极坐标空间中,在直线倾斜的特定角度 ("theta") 和其相对于坐标中心的偏移 ("ro") 的交点处形成峰值。

直线的霍夫变换

直线的霍夫变换

左边(原始)图像上的三个彩色点都在极坐标空间(右边)中留下了轨迹,因为可以从不同角度以及垂直于中心的角度通过一个点绘制无限多条直线。除了红色标记外,每个轨迹片段只被“标记”一次:在这一点上,所有三个轨迹相交并给出最大响应 (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 { ... } // Problem - template!

但是,不能导出模板类型和模板函数。因此,我们将制作一个中间的非模板类 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();
   }
   ...

为了计算“填充的”像素在变换后的尺寸空间(大小 sizex size)中留下的“足迹”统计,我们描述了数据数组 data。辅助模板类 Plain2DArray(使用类型参数 T)允许模拟任意大小的二维数组。使用 double 类型参数的相同类适用于 trigonometric 表(包含预计算的角度正余弦值)。我们需要用该表来快速映射像素到一个新的空间。

检测最突出直线参数的方法称为 extract。它将图像作为输入,并且必须用找到的直线参数对填充输出 result 数组。在下面的等式中:

y = a * x + b

a 参数(斜率,“theta”)将被写入 result 数组的偶数位, 同时 b 参数(偏移,“ro”)将被写入数组的奇数位。例如,该方法完成后第一条最明显的直线由以下表达式描述:

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 degrees, for example
      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); // range [-range, +range]
               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 辅助方法(参见源代码)将新空间中最大值的坐标写入 xy 变量,额外覆盖该位置的邻域,以免反复查找。

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// number of parameters in the model formula
   const string about;  // verbal description
   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];");
}

霍夫变换的各种修改不仅可以揭示直线,还可以揭示对应于给定解析公式的其他结构(例如,圆)。这种修改将揭示不同数量的参数并具有不同的含义。使用自文档化函数更容易集成库(尤其是当有很多库的时候;注意,我们的头文件只包含与实现这个霍夫变换接口的任何库相关的一般信息,而不仅仅是直线信息)。

当然,用公共方法导出类的例子有点片面,因为有可能直接导出变换函数。然而,在实践中,类往往包含更多的功能。特别是,我们很容易在类中添加算法灵敏度的调整、来自线条的示例模式的存储(用于检测在历史数据上验证过的信号)等等。

我们在一个指标中使用库,这个指标根据给定数量柱线的 HighLow 价格来计算支撑线和阻力线。借助霍夫变换和编程接口,该库可以显示数条最重要的支撑线和阻力线。

指标的源代码在 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) 的位置。为了实现图像,我们需要描述从 Hough Image<int> 衍生的 HoughQuotes 类。

我们将以几种方式“绘出”像素:在烛体内,在蜡烛的全范围内,以及直接在高点和低点处。所有这些都在 PRICE_LINE 枚举中形式化了。目前,指标只使用 HighHighLowLow,但这可以在设置中取消。

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) 等于 size 柱线的实际价格范围 (pp) 除以 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 种方法:getWidthgetHeightget。前两种方法很简单。

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

根据从 PRICE_LINE 中选择的计算方法,如果指定的点落在柱线或单元格范围内,则 get 方法基于引号获取“像素”返回 1。否则,返回 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))
   {
      ... // see the next block
      now = iTime(NULL00);
   }
   return rates_total;
}

在不同价格类型形成的一对图像上(highslows),运行两次变化计算。在这种情况下,由同一对象 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);
}

在测试新的开发之前,千万别忘了编译库和指标。

使用默认设置运行指标会得到类似这样的结果。

基于霍夫变换库的具有高/低价主线的指标

基于霍夫变换库的具有高/低价主线的指标

在我们的例子中,测试是成功的。但是如果需要调试库该怎么办?对此没有内置的工具,那么可以使用下面的技巧。库源代码测试被有条件地编译到产品的调试版本中,并且根据构建的库来测试产品。我们来分析指标的例子。

我们提供 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 指令,则可以测试逐步图像分析。