Clases y plantillas en bibliotecas MQL5

Aunque la exportación e importación de clases y plantillas suelen estar prohibidas, el desarrollador puede eludir estas restricciones trasladando la descripción de las interfaces base abstractas al archivo de encabezado de la biblioteca y pasando punteros. Ilustremos este concepto con un ejemplo de una biblioteca que realiza una transformada de Hough de una imagen.

La transformada de Hough es un algoritmo para extraer características de una imagen comparándola con algún modelo formal (fórmula) descrito por un conjunto de parámetros.

La transformada de Hough más sencilla consiste en seleccionar líneas rectas en la imagen mediante su conversión en coordenadas polares. Con este tratamiento, las secuencias de píxeles «rellenos», dispuestos más o menos en fila, forman picos en el espacio de coordenadas polares en la intersección de un ángulo específico («theta») de la inclinación de la recta y su desplazamiento («ro») respecto al centro de coordenadas.

Transformada de Hough para rectas

Transformada de Hough para rectas

Cada uno de los tres puntos en color de la imagen de la izquierda (original) deja un rastro en el espacio de coordenadas polares (derecha) porque se puede trazar un número infinito de líneas rectas a través de un punto en diferentes ángulos y perpendiculares al centro. Cada fragmento de traza se «marca» una sola vez, a excepción de la marca roja: en este punto, las tres trazas se cruzan y dan la respuesta máxima (3). De hecho, como podemos ver en la imagen original, hay una línea recta que pasa por los tres puntos. Así, los dos parámetros de la línea se revelan por el máximo en coordenadas polares.

Podemos utilizar esta transformada de Hough en gráficos de precios para resaltar líneas alternativas de compatibilidad y resistencia. Si estas líneas se suelen trazar en los extremos individuales y, de hecho, realizan un análisis de valores atípicos, entonces las líneas de la transformada de Hough pueden tener en cuenta todos los precios de High o de Low, o incluso la distribución de los volúmenes de ticks dentro de las barras. Todo esto permite obtener una estimación más razonable de los niveles.

Empecemos con el archivo de encabezado LibHoughTransform.mqh. Dado que una imagen abstracta suministra los datos iniciales para el análisis, definamos la plantilla de interfaz HoughImage.

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

Todo lo que se necesita saber sobre la imagen a la hora de procesarla son sus dimensiones y el contenido de cada píxel, que, por razones de generalización, se representa mediante el tipo paramétrico T. Es evidente que, en el caso más sencillo, puede ser int o double.

Llamar al tratamiento analítico de imágenes es un poco más complicado. En la biblioteca, tenemos que describir la clase, cuyos objetos serán devueltos desde una función de fábrica especial (en forma de punteros). Es esta función la que debe exportarse de la biblioteca. Supongamos que es como sigue:

template<typename T>
class HoughTransformDraft
{
public:
   virtual int transform(const HoughImage<T> &imagedouble &result[],
      const int elements = 8) = 0;
};
   
HoughTransformDraft<?> *createHoughTransform() export { ... } // Problem - template!

Sin embargo, los tipos de plantilla y las funciones de plantilla no pueden exportarse. Por lo tanto, haremos una clase intermedia sin plantilla HoughTransform, en la que añadiremos un método de plantilla para el parámetro imagen. Por desgracia, los métodos de plantilla no pueden ser virtuales, y por lo tanto despacharemos manualmente las llamadas dentro del método (usando dynamic_cast), redirigiendo el procesamiento a una clase derivada con un método virtual.

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

La implementación interna de la clase HoughTransformConcrete se escribirá en el archivo de biblioteca 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) { }
   ...

Dado que vamos a recalcular los puntos de la imagen en el espacio en nuevas coordenadas polares, debe asignarse un tamaño determinado a la tarea. Aquí hablamos de una transformada de Hough discreta, ya que consideramos la imagen original como un conjunto discreto de puntos (píxeles), y acumularemos los valores de los ángulos con las perpendiculares en celdas (cuantos). Para simplificar, nos centraremos en la variante con un espacio cuadrado, donde el número de lecturas tanto en el ángulo como en la distancia al centro es igual. Este parámetro se pasa al constructor de la clase.

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();
   }
   ...

Para calcular las estadísticas de la «huella» dejada por los píxeles «llenos» en el espacio de tamaño transformado con dimensiones size por size, describimos el array data. La clase de plantilla auxiliar Plain2DArray (con parámetro de tipo T) permite la emulación de un array bidimensional de tamaños arbitrarios. La misma clase pero con un parámetro de tipo double se aplica a la tabla trigonometric de valores precalculados de senos y cosenos de ángulos. Necesitaremos la tabla para asignar rápidamente píxeles a un nuevo espacio.

El método para detectar los parámetros de las rectas más prominentes se denomina extract. Toma una imagen como entrada y debe llenar el array de salida result con pares de parámetros encontrados de líneas rectas. En la siguiente ecuación:

y = a * x + b

el parámetro a (pendiente, «theta») se escribirá en los números pares del array result, y el parámetro b (sangría, «ro») se escribirán en los números impares del array. Por ejemplo, la primera línea recta más notable tras la realización del método se describe mediante la expresión:

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

Para la segunda línea, los índices aumentarán a 2 y 3, respectivamente, y así sucesivamente, hasta el número máximo de líneas solicitadas (lines). El tamaño del array result es igual al doble del número de líneas.

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

Los bucles anidados sobre los píxeles de la imagen se organizan en el bloque de búsqueda en línea recta. Para cada punto «lleno» (distinto de cero), se realiza un bucle a través de las inclinaciones y se marcan los pares de coordenadas polares correspondientes en el espacio transformado. En este caso, simplemente llamamos al método para aumentar el contenido de la celda en el valor devuelto por el píxel data.inc((int)r, i, v), pero, dependiendo de la aplicación y del tipo T, ello puede requerir un tratamiento más complejo.

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

En la segunda parte del método se realiza la búsqueda de máximos en el nuevo espacio y se rellena el array de salida 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;
   }

El método auxiliar findMax (véase el código fuente) escribe las coordenadas del valor máximo en el nuevo espacio en las variables x y y, sobrescribiendo además la vecindad de este lugar para no encontrarlo una y otra vez.

La clase LinearHoughTransform está lista, y podemos escribir una función de fábrica exportable para generar objetos.

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

Dado que las plantillas no están permitidas para la exportación, utilizamos la enumeración ENUM_DATATYPE en el segundo parámetro para variar el tipo de datos durante la conversión y en la representación original de la imagen.

Para probar la exportación/importación de estructuras, también describimos una estructura con metainformación sobre la transformación en una versión determinada de la biblioteca y exportamos una función que devuelve dicha estructura.

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];");
}

Diversas modificaciones de las transformadas de Hough pueden revelar no sólo líneas rectas, sino también otras construcciones que corresponden a una fórmula analítica dada (por ejemplo, círculos). Dichas modificaciones revelarán un número diferente de parámetros y tendrán un significado distinto. Disponer de una función autodocumentada puede facilitar la integración de bibliotecas (especialmente cuando hay muchas; tenga en cuenta que nuestro archivo de encabezado contiene sólo información general relacionada con cualquier biblioteca que implemente esta interfaz de transformación de Hough, y no sólo para líneas rectas).

Por supuesto, este ejemplo de exportación de una clase con un único método público es un tanto arbitrario porque sería posible exportar directamente la función de transformación. Sin embargo, en la práctica, las clases suelen contener más funcionalidades. En concreto, es fácil añadir a nuestra clase el ajuste de la sensibilidad del algoritmo, el almacenamiento de patrones ejemplares de líneas para detectar señales comprobadas en el historial, etc.

Utilicemos la biblioteca en un indicador que calcula las líneas de compatibilidad y resistencia mediante los precios High y Low en un número determinado de barras. Gracias a la transformada de Hough y a la interfaz de programación, la biblioteca permite visualizar varias de las líneas más importantes de este tipo.

El código fuente del indicador se encuentra en el archivo MQL5/Indicators/MQL5Book/p7/LibHoughChannel.mq5. También incluye el archivo de encabezado LibHoughTransform.mqh, en el que añadimos la directiva de importación.

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

En la imagen analizada, indicamos mediante píxeles la posición de determinados tipos de precios (OHLC) en las cotizaciones. Para implementar la imagen, necesitamos describir la clase HoughQuotes derivada de Hough Image<int>.

Preveremos «pintar» píxeles de varias maneras: dentro del cuerpo de las velas, dentro de todo el rango de las velas, así como directamente en los máximos y mínimos. Todo esto se formaliza en la enumeración PRICE_LINE. Por ahora, el indicador sólo utilizará HighHigh y LowLow, pero esto puede eliminarse en la configuración.

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

En los parámetros del constructor y las variables internas, especificamos el rango de barras para el análisis. El número de barras size determina el tamaño horizontal de la imagen. Para simplificar, utilizaremos el mismo número de lecturas en vertical. Por lo tanto, el paso de discretización de precios (step) es igual al rango real de precios (pp) de las barras size dividido por size. Para la variable base, calculamos el límite inferior de los precios que son objeto de consideración en las barras indicadas. Esta variable será necesaria para enlazar la construcción de líneas basadas en los parámetros encontrados de la transformada de Hough.

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

Recordemos que la interfaz HoughImage requiere la implementación de 3 métodos: getWidth, getHeight y get. Los dos primeros son fáciles.

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

El método get para obtener «píxeles» basados en cotizaciones devuelve 1 si el punto especificado cae dentro del rango de barras o celdas, según el método de cálculo seleccionado de PRICE_LINE. En caso contrario, se devuelve 0. Este método puede mejorarse significativamente mediante la evaluación de fractales, el aumento constante de los extremos o los precios «redondos» con un mayor peso (grasa de píxel).

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

El método auxiliar convert proporciona un recálculo de las coordenadas y del píxel a valores de precio.

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

Ahora todo está listo para escribir la parte técnica del indicador. En primer lugar, vamos a declarar tres variables de entrada para seleccionar el fragmento que se va a analizar y el número de líneas. Todas las líneas se identificarán con un prefijo común.

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

El objeto que proporciona el servicio de transformación se describirá como global: aquí es donde se llama a la función de fábrica createHoughTransform desde la biblioteca.

HoughTransform *ht = createHoughTransform(BarCount);

En la función OnInit, nos limitamos a registrar la descripción de la biblioteca utilizando la segunda función importada getHoughInfo.

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

Realizaremos el cálculo en OnCalculate una vez, en la apertura de la barra.

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

El cálculo de la transformación propiamente dicho se ejecuta dos veces en un par de imágenes (highs y lows) formadas por distintos tipos de precios. En este caso, el trabajo lo realiza secuencialmente el mismo objeto ht. Si la detección de líneas rectas ha sido satisfactoria, las mostramos en el gráfico mediante la función DrawLine. Dado que las líneas se enumeran en el array de resultados en orden descendente de importancia, se les asigna un peso decreciente.

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

La función DrawLine se basa en objetos gráficos de tendencia (OBJ_TREND, véase el código fuente).

Al desinicializar el indicador, borramos las líneas y el objeto analítico.

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

Antes de probar un nuevo desarrollo, no olvide compilar tanto la biblioteca como el indicador.

La ejecución del indicador con la configuración predeterminada da como resultado algo como esto:

Indicador con líneas principales para precios altos/bajos basado en la biblioteca de transformadas de Hough

Indicador con líneas principales para precios altos/bajos basado en la biblioteca de transformadas de Hough

En nuestro caso, la prueba ha sido un éxito. Pero, ¿y si necesita depurar la biblioteca? No existen herramientas integradas para ello, por lo que se puede utilizar el siguiente truco. La prueba del código fuente de la biblioteca se compila condicionalmente en una versión de depuración del producto, y éste se prueba con la biblioteca creada. Consideremos el ejemplo de nuestro indicador.

Vamos a proporcionar la macro LIB_HOUGH_IMPL_DEBUG para permitir la integración de la fuente de la biblioteca directamente en el indicador. La macro debe colocarse antes de incluir el archivo de encabezado.

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

En el propio archivo de encabezado, superpondremos el bloque de importación de la copia binaria independiente de la biblioteca con instrucciones de compilación condicional del preprocesador. Cuando la macro esté activada, se ejecutará otra rama, con la sentencia #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

En el archivo fuente de la biblioteca LibHoughTransform.mq5, dentro de la función getHoughInfo, añadimos salida al registro de información sobre el método de compilación, dependiendo de si la macro está activada o desactivada.

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];");
}

Si en el código del indicador, en el archivo LibHoughChannel.mq5, elimina el comentario de la instrucción #define LIB_HOUGH_IMPL_DEBUG, puede probar el análisis de imagen paso a paso.