English Русский 中文 Deutsch 日本語 Português
Simulador de estrategias personalizado basado en cálculos matemáticos rápidos

Simulador de estrategias personalizado basado en cálculos matemáticos rápidos

MetaTrader 5Ejemplos | 5 febrero 2018, 15:31
5 085 0
Vasiliy Sokolov
Vasiliy Sokolov

Índice



Introducción

El simulador de estrategias suministrado por MetaTrader 5 posee una potente funcionalidad para la resolución de diversas tareas. Con su ayuda, se pueden simular tanto estrategias comerciales complejas con cestas de instrumentos, como estrategias individuales con reglas sencillas de entrada y salida. Sin embargo, esta funcionalidad tan amplia no nos va a hacer falta con tanta frecuencia. En ocasiones, solo necesitamos comprobar rápidamente alguna sencilla idea comercial o hacer un cálculo aproximado, cuya precisión se verá compensada por su velocidad. El simulador estándar de MetaTrader 5 tiene una capacidad interesante, pero poco conocida: sabe realizar cálculos en el modo de cálculos matemáticos. Se trata de un modo limitado para iniciar el simulador de estrategias, que, sin embargo, posee todas las ventajas de la simulación completa: en este se encuentran disponibles los cálculos en la nube, se puede utilizar el optimizador genético y existe la posibilidad de reunir los tipos personalizados de datos.

Un simulador de estrategias propio podría venirle bien no solo a aquellos que necesitan velocidad absoluta. La simulación en el modo de cálculos matemáticos también abre un cierto camino a los investigadores. El simulador estándar permite modelar operaciones comerciales muy próximas a la realidad. En el modo de investigación, esta exigencia no siempre resulta útil. Por ejemplo, a veces es necesario obtener una valoración de la efectividad pura de un sistema comercial, sin tener en cuenta el deslizamiento, los spreads y la comisión. El simulador de cálculos matemáticos desarrollado en este artículo nos ofrece esta posibilidad.

Por supuesto, es imposible abarcar lo inabarcable. Este artículo no es una excepción. La escritura de un simulador de estrategias propio requiere de un trabajo serio y meticuloso. Nuestro objetivo es más modesto: vamos a mostrar que, usando las bibliotecas necesarias, crear nuestro porpio simulador de estrategias no es algo tan complejo como podría parecer a primera vista.

Si el tema le resulta interesante a los lectores, este artículo tendrá una continuación en la que se desarrollarán las ideas propuestas.


Introducción al modo de cálculos matemáticos

El modo de cálculos matemáticos se inicia en la ventana del simulador de estrategias. Para ello, hay que elegir en la lista desplegable el punto homónimo en el menú:

Fig. 1 Selección del modo de cálculos matemáticos en el simulador de estrategias

En este modo se llama solo un conjunto limitado de funciones, mientras que el entorno comercial (símbolos, información sobre la cuenta, propiedades del servidor comercial) no está disponible. La principal función de la llamada es OnTester(), a través de la cual el usuario puede indicar un criterio de optimización personalizado especial. Este se usará junto con otros criterios de optmización estándar y estará disponible para su representación en el informe personalizado estándar de la estrategia. En la captura de pantalla que vemos más abajo, se destaca con un marco rojo:

 

Fig. 2. Criterio de optimización personalizado, calculado en la función OnTester

Los valores retornados por la función OnTester se pueden someter a iteración y optimización. Vamos a demostrar esto en un asesor simplísimo:

//+------------------------------------------------------------------+
//|                                                OnTesterCheck.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input double x = 0.01;
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
{
   double ret = MathSin(x);
   return(ret);
}
//+------------------------------------------------------------------+

En su código no hay nada, aparte del parámetro x y la función OnTester, que calcula el valor del seno a partir del argumento transmitido, en este caso es x. Vamos a intentar ahora optimizar esta función. Para ello, en el simulador de estrategias elegiremos el tipo de optimización "Lenta (iteración completa de los parámetros)", y dejaremos el modo de simulación existente: "cálculos matemáticos".

En los parámetros de optimización, indicaremos el rango de cambio de x: valor inicial — 0.01, salto — 0.01, stop — 10. Después de que todo esté listo, iniciaremos el simulador de estrategias. Este realizará su trabajo casi al instante. Después de ello, abriremos el gráfico de optimización y elegiremos el tipo "Gráfico lineal" en el menú contextual. Ante nosotros, tendremos la función del seno en su interpretación gráfica:

Fig. 3. Representación gráfica de la función de seno

Una peculiaridad estupenda de este modo es tiene un gasto de recursos muy bajo. Las operaciones de lectura-guardado del disco duro se han reducido al mínimo, los agentes de simulación no cargan las cotizaciones de los símbolos solicitados, no existe nigún cálculo adicional, y todos los cálculos se centran en la función OnTester. 

Teniendo en cuenta el buen potencial de la velocidad de OnTester, podemos crear un módulo computacional autosuficiente, que permita realizar simulaciones sencillas. Aquí tenemos los elementos de este módulo:

  • Historia del instrumento en el que se realizará la simulación
  • Sistema de posiciones virtuales
  • Sistema comercial encargado de la gestión de las posiciones virtuales
  • Sistema de análisis de resultados

La autosuficiencia del módulo implica que en un solo experto se encontrarán todos los datos necesarios para la simulación y la propia simulación que los utilizará. Tal experto puede expandirse fácilmente en una red de computación distribuida, si es necesaria su optimización en la nube.

Vamos a pasar a la descripción de la primera parte del sistema, concretamente, al almacenamiento de la historia para la simulación.


Almacenamiento de la historia para el simulador de cálculos matemáticos

El modo de cálculos matemáticos no presupone acceso a los instrumentos financieros. La llamada de funciones como CopyRates(Symbol(),...) aquí pierde su sentido. Sin embargo, los datos históricos son imprescindibles para la simulación. Para ello, podemos guardar la historia de cotizaciones del símbolo necesario en una matriz del tipo uchar[] previamente comprimida:

uchar symbol[128394] = {0x98,0x32,0xa6,0xf7,0x64,0xbc...};

Cualquier tipo de datos: sonidos, imágenes, cifras y líneas, se puede imaginar como un conjunto sencillo de bytes. Un byte es un bloque corto que consta de ocho bits. Cualquier información se guarda en "paquetes", en una secuencia que consta de estos bytes. En MQL5 existe el tipo especial de datos uchar. Cada uno de sus valores puede representar un byte exacto. De esta forma, una matriz uchar con unas dimensiones de 100 elementos puede almacenar 100 bytes.

Las cotizaciones del símbolo constan de conjuntos de barras. Cada barra incluye información sobre la hora de apertura de la barra, sus precios (máximo, mínimo, apertura y cierre) y su volumen. Cada valor de este tipo se guarda en la variable de longitud correpondiente. Vamos a mostrar un recuadro:

Valor Tipo de datos Tamaño en bytes
Hora de apertura datetime 8
Precio de apertura double 8
Precio máximo double 8
Precio mínimo double 8
Precio de cierre double 8
Volumen de ticks long  8
Spread   int 4
Volumen real   long 8

No resulta complicado calcular que para guardar una barra serán necesarios 60 bytes, o una matriz uchar que conste de 60 elementos. Para el mercado Fórex de 24 horas, un día comercial consta de 1440 barras de minuto. Por consiguiente, la historia de minutos de un año constará aproximadamente de 391680 barras. Multiplicando esta cifra por 60 bytes, descubriremos que un año de historia de minutos en su forma no comprimida supone aproximadamente 23 Mb. ¿Es mucho o poco? Según las medidas modernas, no es mucho, pero imagine lo que pasará si decidimos simular el experto con los datos de los últimos 10 años. Tendremos que almacenar 230 Mb de datos, e incluso puede ser que haya que difundirlos por la red. Esto sí que representa un volumen significativo, incluso para las medidas modernas.

Por eso, tenemos que comprimir esta información de alguna forma. Por fortuna, ya hace bastante tiempo que se escribió una biblioteca especial para trabajar con archivos Zip. Aparte de poseer esta funcionalidad, esta biblioteca permite convertir el resultado de la compresión en una matriz de datos, lo que simplificará significativamente nuestro trabajo.

Bien, nuestro algortimo cargará la matriz de barras MqlRates, la convertirá en una representación de bytes y después la comprimirá con la ayuda de Zip, guardando los resultados obtenidos en forma de matriz uchar definida en el archivo mqh.

Para covertir las cotizaciones en una matriz de bytes, se usa un sistema de conversión que combina tipos union. Este sistema permite colocar varios tipos de datos en un solo campo de almacenamiento. De este modo, recurriendo al propio tipo, podremos obtener los datos de otro. En esta combinación guardaremos dos tipos: la estructura MqlRates y la matriz uchar, cuyo número de elementos será igual al tamaño de MqlRates. Para comprender cómo funciona este sistema, recurriremos a la primera versión de nuestro script SaveRates.mq5, que convierte la historia del instrumento en una matriz de bytes uchar:

//+------------------------------------------------------------------+
//|                                                    SaveRates.mq5 |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Zip\Zip.mqh>
#include <ResourceCreator.mqh>
input ENUM_TIMEFRAMES MainPeriod;

union URateToByte
{
   MqlRates bar;
   uchar    bar_array[sizeof(MqlRates)];
}RateToByte;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   //-- Cargamos las cotizaciones
   MqlRates rates[];
   int total = CopyRates(Symbol(), Period(), 0, 20000, rates);
   uchar symbol_array[];
   //-- Las convertimos en una imagen de bytes
   ArrayResize(symbol_array, sizeof(MqlRates)*total);
   for(int i = 0, dst = 0; i < total; i++, dst +=sizeof(MqlRates))
   {
      RateToByte.bar = rates[i];
      ArrayCopy(symbol_array, RateToByte.bar_array, dst, 0, WHOLE_ARRAY);
   }
   //-- Las comprimimos en un archivo zip
   CZip Zip;
   CZipFile* file = new CZipFile(Symbol(), symbol_array);
   Zip.AddFile(file);
   uchar zip_symbol[];
   //-- Obtenemos la representación de bytes del fichero comprimido
   Zip.ToCharArray(zip_symbol);
   //-- Lo almacenamos en forma de archivo mqh de inclusión
   CCreator creator;
   creator.ByteArrayToMqhArray(zip_symbol, "rates.mqh", "rates");
}
//+------------------------------------------------------------------+

Después de obtener este código, en la matriz zip_symbol se encontrará la matriz comprimida de estructuras MqlRates: la historia comprimida de cotizaciones. A continuación, la matriz se guarda en forma de archivo mqh en el disco duro de la computadora. Más abajo hablaremos sobre cómo se hace esto y por qué es necesario. 

Obtener una representación de bytes de las cotizaciones y compimirlas no es suficiente. Hay que almacenar esta representación en forma de matriz uchar. En este caso, además, es necesario que se cargue en forma de recurso, es decir, tiene que ser compilado junto con el programa. Con este objetivo, hemos creado un archivo de encabezado mqh especial que contiene nuestra matriz como un conjunto sencillo de símbolos ASCII. Para ello, utilizaremos la clase especial CResourceCreator:

//+------------------------------------------------------------------+
//|                                              ResourceCreator.mqh |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Arrays\ArrayObj.mqh>
//+------------------------------------------------------------------------+
//| Contiene los identificadores de línea de la matriz de recursos creada  |
//+------------------------------------------------------------------------+
class CResInfo : public CObject
{
public:
   string FileName;
   string MqhFileName;
   string ArrayName;
};
//+------------------------------------------------------------------+
//| Crea un recurso MQL en forma de matriz de bytes.                 |
//+------------------------------------------------------------------+
class CCreator
{
private:
   int      m_common;
   bool     m_ch[256];
   string   ToMqhName(string name);
   void     CreateInclude(CArrayObj* list_info, string file_name);
public:
            CCreator(void);
   void     SetCommonDirectory(bool common);
   bool     FileToByteArray(string file_name, uchar& byte_array[]);
   bool     ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name);
   void     DirectoryToMqhArray(string src_dir, string dst_dir, bool create_include = false);
};
//+------------------------------------------------------------------+
//| Constructor por defecto                                          |
//+------------------------------------------------------------------+
CCreator::CCreator(void) : m_common(FILE_COMMON)
{
   ArrayInitialize(m_ch, false);
   for(uchar i = '0'; i < '9'; i++)
      m_ch[i] = true;
   for(uchar i = 'A'; i < 'Z'; i++)
      m_ch[i] = true;
}
//+------------------------------------------------------------------+
//| Establece la bandera FILE_COMMON, o bien la quita                |
//+------------------------------------------------------------------+
CCreator::SetCommonDirectory(bool common)
{
   m_common = common ? FILE_COMMON : 0;   
}

//+-----------------------------------------------------------------------------------+
//| Pasa todas los archivos en el directorio src_dir a los archivos mqh que contienen |
//| la representación de bytes de estos archivos                                      |
//+-----------------------------------------------------------------------------------+
void CCreator::DirectoryToMqhArray(string src_dir,string dst_dir, bool create_include = false)
{
   string file_name;
   string file_mqh;
   CArrayObj list_info;
   long h = FileFindFirst(src_dir+"\\*", file_name, m_common);
   if(h == INVALID_HANDLE)
   {
      printf("El directorio" + src_dir + " no ha sido encontrado, o bien no contiene archivos");
      return;
   }
   do
   {
      uchar array[];
      if(FileToByteArray(src_dir+file_name, array))
      {
         string norm_name = ToMqhName(file_name);
         file_mqh = dst_dir + norm_name + ".mqh";
         ByteArrayToMqhArray(array, file_mqh, "m_"+norm_name);
         printf("Create resource: " + file_mqh);
         //Añadimos la información sobre el recurso creado
         CResInfo* info = new CResInfo();
         list_info.Add(info);
         info.FileName = file_name;
         info.MqhFileName = norm_name + ".mqh";
         info.ArrayName = "m_"+norm_name;
      }
   }while(FileFindNext(h, file_name));
   if(create_include)
      CreateInclude(&list_info, dst_dir+"include.mqh");
}
//+-------------------------------------------------------------------------------+
//| Crea un archivo mqh con las inclusiones de todos los archivos generados       |
//+-------------------------------------------------------------------------------+
void CCreator::CreateInclude(CArrayObj *list_info, string file_name)
{
   int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common);
   if(handle == INVALID_HANDLE)
   {
      printf("No se ha logrado crear el archivo include" + file_name);
      return;
   }
   //Creamos el enzabezado include
   for(int i = 0; i < list_info.Total(); i++)
   {
      CResInfo* info = list_info.At(i);
      string line = "#include \"" + info.MqhFileName + "\"\n";
      FileWriteString(handle, line);
   }
   //Creamos una función que copia la matriz del recurso
   FileWriteString(handle, "\n");
   FileWriteString(handle, "void CopyResource(string file_name, uchar &array[])\n");
   FileWriteString(handle, "{\n");
   for(int i = 0; i < list_info.Total(); i++)
   {
      CResInfo* info = list_info.At(i);
      if(i == 0)
         FileWriteString(handle, "   if(file_name == \"" + info.FileName + "\")\n");
      else
         FileWriteString(handle, "   else if(file_name == \"" + info.FileName + "\")\n");
      FileWriteString(handle,    "      ArrayCopy(array, " + info.ArrayName + ");\n");
   }
   FileWriteString(handle, "}\n");
   FileClose(handle);
}
//+------------------------------------------------------------------------------+
//| convierte el nombre tansmitido en el nombre correcto de la variable MQL      |
//+------------------------------------------------------------------------------+
string CCreator::ToMqhName(string name)
{
   uchar in_array[];
   uchar out_array[];
   int total = StringToCharArray(name, in_array);
   ArrayResize(out_array, total);
   int t = 0;
   for(int i = 0; i < total; i++)
   {
      uchar ch = in_array[i];
      if(m_ch[ch])
         out_array[t++] = ch;
      else if(ch == ' ')
         out_array[t++] = '_';
      uchar d = out_array[t-1];
      int dbg = 4;
   }
   string line = CharArrayToString(out_array, 0, t);
   return line;
}
//+------------------------------------------------------------------------------+
//| Retorna la representación de bytes del archivo transmitido en forma de       |
//| matriz byte_array                                                            |
//+------------------------------------------------------------------------------+
bool CCreator::FileToByteArray(string file_name, uchar& byte_array[])
{
   int handle = FileOpen(file_name, FILE_READ|FILE_BIN|m_common);
   if(handle == -1)
   {
      printf("Filed open file " + file_name + ". Reason: " + (string)GetLastError());
      return false;
   }
   FileReadArray(handle, byte_array, WHOLE_ARRAY);
   FileClose(handle);
   return true;
}
//+--------------------------------------------------------------------------+
//| Convierte la matriz de bytes transmitida byte_array en el archivo mqh    |
//| file_name en el que se ubica la descripción de la matriz con el nombre   |
//| array_name                                                               |
//+--------------------------------------------------------------------------+
bool CCreator::ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name)
{
   int size = ArraySize(byte_array);
   if(size == 0)
      return false;
   int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common, "");
   if(handle == -1)
      return false;
   string strSize = (string)size;
   string strArray = "uchar " +array_name + "[" + strSize + "] = \n{\n";
   FileWriteString(handle, strArray);
   string line = "   ";
   int chaptersLine = 32;
   for(int i = 0; i < size; i++)
   {
      ushort ch = byte_array[i];
      line += (string)ch;
      if(i == size - 1)
         line += "\n";
      if(i>0 && i%chaptersLine == 0)
      {
         if(i < size-1)
            line += ",\n";
         FileWriteString(handle, line);
         line = "   ";
      }
      else if(i < size - 1)
         line += ",";
   }
   if(line != "")
      FileWriteString(handle, line);
   FileWriteString(handle, "};");
   FileClose(handle);
   return true;
}

No vamos a detenernos con detalle en su funcionamiento, solo lo describiremos en líneas generales, enumerando también sus posibilidades.

  • Lee cualquier archivo aleatorio en el disco duro y lo guarda como representación de bytes en forma de matriz uchar en un archivo mqh
  • Lee un directorio aleatorio en el disco duro y guarda la representación de bytes de todos los archivos en este directorio. La representación de bytes para cada archivo de este tipo se encuentra en un archivo mqh aparte, que contiene una matriz uchar
  • Recibe en la entrada una matriz de bytes uchar y la guarda en forma de matriz de símbolos en un archivo mqh aparte
  • Crea un archivo de encabezado especial que contiene enlaces a todos los archivos mqh creados en el proceso de generación. Asimismo, se crea una función especial que recibe a la entrada el nombre de la matriz y retorna su representación de bytes. Este algoritmo usa la generación dinámica de código. 

La clase descrita es una potente alternativa al sistema estándar de ubicación de recursos en un programa mql.

Por defecto, todas las operaciones con archivos tienen en lugar en el directorio general de archivos (FILE_COMMON). Si ejecutamos el script de la lista anterior, en él aparecerá un nuevo archivo rates.mqh (el nombre del archivo se indica con el segundo parámetro del método ByteArrayToMqhArray). Contendrá la gigantesca matriz rates[] (el nombre de la matriz se indica con el tercer parámetro de este método). Aquí tenemos un fragmento del contenido de este archivo:


Fig. 4. Cotizaciones de MqlRates en forma de matriz de bytes rates comprimida

La compresión de datos funciona de maravilla. Un año de historia de minutos sin comprimir de la pareja de divisas EURUSD ocupa aproximadamente 20 Mb, después de la compresión, solo 5 Mb. Sin embargo, es mejor no abrir el propio rates.mqh en el MetaEditor: su tamaño es sustancialmente superior a esta cifra, y el editor puede quedaerse colgado. Pero no se preocupe. Después de la compilación, el texto se transforma en bytes, y el tamaño factual del programa aumenta solo en la magnitud real de la información almacenada, es decir, en 5 megabytes, en nuestro caso.

Por cierto, con ayuda de esta tecnología, es posible guardar en un programa ex5 cualquier tipo de información que necesitemos, no solo la historia de cotizaciones.


Cargando las cotizaciones de MqlRates desde una matriz de bytes

Ahora que la historia ha sido guardada, podemos incluirla en cualquier programa mql, para ello, basta con escribir la directiva include en su comienzo:

...
#include "rates.mqh"
...

En este caso, además, rates.mqh debe reubicarse en el direcotorio de textos originales del propio programa.

Incluir los datos no es suficiente. Además, hay que escribir un procedimiento para su transformación inversa a una matriz MqlRates normal. Para ello, implementaremos la función LoadRates. A la entrada, recibirá la matriz vacía MqlRates según un enlace. Al finalizar su funcionamiento, en la matriz se contendrán las cotizaciones habituales de MqlRates, cargadas desde la matriz comprimida. Aquí tenemos el código de esta función:

//+------------------------------------------------------------------+

//|                                                      Mtester.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include <Zip\Zip.mqh>
#include "rates.mqh"
//+------------------------------------------------------------------+
//| Casting MqlRates to uchar[]                                      |
//+------------------------------------------------------------------+
union URateToByte
{
   MqlRates bar;
   uchar    bar_array[sizeof(MqlRates)];
};
//+---------------------------------------------------------------------------+
//| Transforma los datos comprimidos en una matriz de cotizaciones MqlRates   |
//| Retorna el número de barras obtenidas; retorna -1 en caso                 |
//| de no tener éxito                                                         |
//+---------------------------------------------------------------------------+
int LoadRates(string symbol_name, MqlRates &mql_rates[])
{
   CZip Zip;
   Zip.CreateFromCharArray(rates);
   CZipFile* file = dynamic_cast<CZipFile*>(Zip.ElementByName(symbol_name));
   if(file == NULL)
      return -1;
   uchar array_rates[];
   file.GetUnpackFile(array_rates);
   URateToByte RateToBar;
   ArrayResize(mql_rates, ArraySize(array_rates)/sizeof(MqlRates));
   for(int start = 0, i = 0; start < ArraySize(array_rates); start += sizeof(MqlRates), i++)
   {
      ArrayCopy(RateToBar.bar_array, array_rates, 0, start, sizeof(MqlRates));
      mql_rates[i] = RateToBar.bar;
   }
   return ArraySize(mql_rates);
}
//+------------------------------------------------------------------+

La función se encuentra en el archivo Mtester.mqh. Esta será nuestra primera función para trabajar en el modo de cálculos matemáticos. Más tarde, se añadirá al archivo Mtester.mqh una nueva funcionalidad, y posiblemente se convierta en un motor completo para la simulación matemática de estrategias.

Vamos a escribir una estrategia elemental para el modo de cálculos matemáticos. Solo ejecutará dos funciones: cargar las cotizaciones en las funciones OnInit y calcular el valor medio de todos los precios de cierre en las funciones OnTester. El resultado del cálculo se retornará a MetaTrader:

//+------------------------------------------------------------------+
//|                                                      MExpert.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include "Mtester.mqh"
//+------------------------------------------------------------------+
//| Cotizaciones con las que va a tener lugar la simulación          |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //-- Cargamos las cotizaciones para el símbolo indicado.
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("No se han encontrado las cotizaciones para el símbolo " + Symbol() + ". Cree el recurso de cotizaciones correspondiente.");
      return INIT_PARAMETERS_INCORRECT;
   }
   printf("Se han cargado " + (string)ArraySize(Rates) + " barras del símbolo " + Symbol());
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
{
   double mean = 0.0;
   for(int i = 0; i < ArraySize(Rates); i++)
      mean += Rates[i].close;
   mean /= ArraySize(Rates);
   return mean;
}
//+------------------------------------------------------------------+

Después de compilar el experto, lo cargamos en el simulador de estrategias y elegimos el modo "Cálculos matemáticos". Iniciamos su simulación y echamos un vistazo al registro:

2017.12.13 15:12:25.127 Core 2  math calculations test of Experts\MTester\MExpert.ex5 started
2017.12.13 15:12:25.127 Core 2  Se han cargado 354159 barras del símbolo EURUSD
2017.12.13 15:12:25.127 Core 2  OnTester result 1.126596405653942
2017.12.13 15:12:25.127 Core 2  EURUSD,M15: mathematical test passed in 0:00:00.733 (total tester working time 0:00:01.342)
2017.12.13 15:12:25.127 Core 2  217 Mb memory used

Como podemos ver, el experto ha funcionado como planeábamos. Se han cargado correctamente todas las cotizaciones, lo cual viene confirmado por el registro sobre la calidad de las barras cargadas. Asimismo, se han iterado correctamente todas las barras para el cálculo del valor medio, que precisamente ha sido retornado al flujo de llamadas. Como podemos ver, el precio medio de todas las cotizaciones de EURUSD en el último año ha sido de 1.12660.


Prototipo de una estrategia basada en la media móvil

Hemos conseguido resultados impresionantes: hemos obtenido los datos y los hemos comprimdo, los hemos guardado en forma de matriz estática uchar y hemos cargado esta en el experto, hemos descomprimido los datos y los hemos convertido de nuevo en una matriz de cotizaciones. Ahora ha llegado el momento de escribir información nueva y útil. Vamos a recurrir a la variante clásica, basada en el cruce de dos medias móviles. Esta estrategia es fácil de implementar. Dado que el entorno comercial en el modo de cálculos matemáticos no está a nuestra disposición, no podemos llamar los indicadores como iMA directamente. En lugar de ello, deberemos calcular el valor de la media manualmente. La principal tarea en este modo de simulación es conseguir la máxima aceleración. Por eso todos los algoritmos que vamos a utilizar deberán funcionar rápido. Ya sabemos que el cálculo de la media pertence a la clase de tareas sencillas, con una velocidad de ejecución de O(1). Esto significa que la velocidad de cálculo del valor medio no deberá depender del periodo de la media. Para estos objetivos, usaremos la biblioteca de búfer circular ya preparada. Ya se ha escrito información más detallada sobre este algoritmo en un artículo aparte.

Para comenzar, escribiremos la plantilla de nuestro primer experto:

//+------------------------------------------------------------------+
//|                                                      MExpert.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include "Mtester.mqh"
#include <RingBuffer\RiSma.mqh>

input int PeriodFastMa = 9;
input int PeriodSlowMa = 16;

CRiSMA FastMA;    // Búfer circular para el cálculo de la media móvil rápida
CRiSMA SlowMA;    // Búfer circular para el cálculo de la media móvil lenta
//+------------------------------------------------------------------+
//| Cotizaciones con las que va a tener lugar la simulación          |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //-- Comprobamos que la combinación de los parámetros sea correcta
   //-- La media rápida no puede ser mayor que la lenta
   if(PeriodFastMa >= PeriodSlowMa)
      return INIT_PARAMETERS_INCORRECT;
   //-- Inicializamos los periodos de los búferes circulares
   FastMA.SetMaxTotal(PeriodFastMa);
   SlowMA.SetMaxTotal(PeriodSlowMa);
   //-- Cargamos las cotizaciones para el símbolo indicado.
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("No se han encontrado las cotizaciones para el símbolo " + Symbol() + ". Cree el recurso de cotizaciones correspondiente.");
      return INIT_FAILED;
   }
   printf("Se han cargado " + (string)ArraySize(Rates) + " barras del símbolo " + Symbol());
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Describimos la estrategia                                        |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates); i++)
   {
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      //Aquí se encontrará la lógica de nuestro experto
   }
   return 0.0;
}
//+------------------------------------------------------------------+

En ella se definen los dos parámetros con los periodos de promediación de la МА rápida y lenta. A continuación, se declaran dos búferes circulares que calculan los valores de estas medias. En el bloque de incialización se comprueba que los parámetros introducidos sean correctos. Dado que los parámetros no son establecidos por el usuario, sino que son elegidos automáticamente por el simulador de estrategias en el modo de optimización, los ajustes a menudo no se combinan adecuadamente. En este caso, nuestra МА rápida puede resultar mayor a la lenta. Para evitar estas confusiones y también ahorrar tiempo en la optimización, finalizaremos esta pasada antes de su inicio. Para ello, en el bloque OnInit se retorna la constante INIT_PARAMETERS_INCORRECT.

Después de inicializar los búferes, comprobar los parámetros y cargar las cotizaciones, llega el momento de ejecutar la propia simulación: iniciamos la función OnTester. En ella, la simulación se encontrará dentro del bloque for. Por el código se puede ver que si el valor medio del búfer circular FastMA es mayor al valor medio SlowMA, será necesario abrir una posición larga, y al contrario. Sin embargo, ahora no tenemos un módulo comercial que pueda abrir estas posiciones largas y cortas. Aún tenemos que escribirlo. 


Clase de la posición virtual

Como ya hemos mencionado con anterioridad, el modo de cálculos matemáticos no está adaptado al cálculo de estrategias. Por ello, también carece de funciones comerciales. Tampoco podemos utilizar el entorno comercial de MetaTrader. En este modo, es inasumible el concepto "posición", simplemente, no existe. Por eso, necesitaremos crear nuestro propio análogo de las posiciones de MetaTrader. Dicho análogo contendrá solo la información imprescindible. Para ello, escribiremos una clase con los campos siguientes: 

  • hora de apertura de la posición;
  • precio de apertura de la posición;
  • hora de cierre de la posición;
  • precio de cierre de la posición;
  • volumen de la posición;
  • spread en el momento de la apertura de la posición;
  • dirección de la posición.

Tal vez en el futuro lo complementemos con información adicional, pero ahora tenemos suficiente con estos campos.

//+------------------------------------------------------------------+
//|                                                      Mtester.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include "rates.mqh"
#include "Type2Char.mqh"
//+-------------------------------------------------------------------------+
//| Clase de la posición virtual para el simulador de cálculos matemáticos  |
//+-------------------------------------------------------------------------+
class CMposition : public CObject
{
private:
   datetime    m_time_open;
   datetime    m_time_close;
   double      m_price_open;
   double      m_price_close;
   double      m_volume;
   int         m_spread;
   ENUM_POSITION_TYPE m_type;
public:
               CMposition(void);
   static int  Sizeof(void);
   bool        IsActive(void);
   datetime    TimeOpen(void);
   datetime    TimeClose(void);
   double      PriceOpen(void);
   double      PriceClose(void);
   double      Volume(void);
   double      Profit(void);
   ENUM_POSITION_TYPE PositionType(void);
   static CMposition*  CreateOnBarOpen(MqlRates& bar, ENUM_POSITION_TYPE pos_type, double vol);
   void        CloseOnBarOpen(MqlRates& bar);
};
//+------------------------------------------------------------------+
//| La posición CMposition ocupa 45 bytes de datos                   |
//+------------------------------------------------------------------+
int CMposition::Sizeof(void)
{
   return 48;
}
CMposition::CMposition(void):m_time_open(0),
                             m_time_close(0),
                             m_price_open(0.0),
                             m_price_close(0.0),
                             m_volume(0.0)
{
}
//+------------------------------------------------------------------+
//| Verdadero, si la posición está cerrada                           |
//+------------------------------------------------------------------+
bool CMposition::IsActive()
{
   return m_time_close == 0;
}
//+------------------------------------------------------------------+
//| Hora de apertura de la posición                                  |
//+------------------------------------------------------------------+
datetime CMposition::TimeOpen(void)
{
   return m_time_open;
}
//+------------------------------------------------------------------+
//| Hora de cierre de la posición                                    |
//+------------------------------------------------------------------+
datetime CMposition::TimeClose(void)
{
   return m_time_close;
}
//+------------------------------------------------------------------+
//| Precio de apertura de la posición                                |
//+------------------------------------------------------------------+
double CMposition::PriceOpen(void)
{
   return m_price_open;
}
//+------------------------------------------------------------------+
//| Precio de cierre de la posición                                  |
//+------------------------------------------------------------------+
double CMposition::PriceClose(void)
{
   return m_price_close;
}
//+------------------------------------------------------------------+
//| Volumen de la posición                                           |
//+------------------------------------------------------------------+
double CMposition::Volume(void)
{
   return m_volume;
}
//+------------------------------------------------------------------+
//| Retorna el tipo de la posición comercial                         |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE CMposition::PositionType(void)
{
   return m_type;
}
//+------------------------------------------------------------------+
//| Beneficio de la posición                                         |
//+------------------------------------------------------------------+
double CMposition::Profit(void)
{
   if(IsActive())
      return 0.0;
   int sign = m_type == POSITION_TYPE_BUY ? 1 : -1;
   double pips = (m_price_close - m_price_open)*sign;
   double profit = pips*m_volume;
   return profit;
}
//+------------------------------------------------------------------+
//| Crea una posición partiendo de los parámetros transmitidos       |
//+------------------------------------------------------------------+
static CMposition* CMposition::CreateOnBarOpen(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume)
{
   CMposition* position = new CMposition();
   position.m_time_open = bar.time;
   position.m_price_open = bar.open;
   position.m_volume = volume;
   position.m_type = pos_type;
   return position;
}
//+------------------------------------------------------------------+
//| Cierra la posición al precio de apertura de la barra transmitida |
//+------------------------------------------------------------------+
void CMposition::CloseOnBarOpen(MqlRates &bar)
{
   m_price_close = bar.open;
   m_time_close = bar.time;
}
//+------------------------------------------------------------------+

El aspecto de mayor interés en esta implementación lo representa el momento de creación de la posición. Sus campos están protegidos contra el cambio externo, sin embargo, el método estático CreateOnBarOpen retorna el objeto de la clase con los parámetros correctamente establecidos. No es posible crear un objeto de esta clase de otra forma que no sea recurriendo a este método. De esta manera, se implementa la protección contra los cambios no intencionados.


Clase del bloque comercial

Ahora tenemos que crear una clase que gestione estas posiciones. Será un análogo de las funciones comerciales de MetaTrader. Resulta obvio que en este módulo deberemos guardar también las propias posiciones. Para ello, se han preparado dos colecciones CArrayObj: la primera es Active — necesaria para almacenar las posiciones activas de la estrategia, la otra es History — que contendrá las posiciones históricas.

Asimismo, en la clase tendremos métodos especiales para la apertura y el cierre de posiciones:

  • EntryAtOpenBar — abre una posición con la dirección y el volumen requerido;
  • CloseAtOpenBar — cierra una posición según el índice elegido.

Las posiciones se abrirán y cerrarán según los precios de la barra transmitida. Este enfoque, por desgracia, no impide echar un vistazo al futuro, aunque es sencillo de implementar y muy rápido.

Nuestra clase CMtrade (así la vamos a llamar), ha resultado bastante sencilla:

//+------------------------------------------------------------------+
//|                                                      Mtester.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include <Arrays\ArrayObj.mqh>
#include "Mposition.mqh"
//+------------------------------------------------------------------+
//| Módulo comercial de apertura de posiciones virtuales             |
//+------------------------------------------------------------------+
class CMtrade
{
public:
               CMtrade(void);
               ~CMtrade();
   CArrayObj   Active;
   CArrayObj   History;
   void        EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume);
   void        CloseAtOpenBar(MqlRates &bar, int pos_index);
};
//+------------------------------------------------------------------+
//| Constructor por defecto                                          |
//+------------------------------------------------------------------+
CMtrade::CMtrade(void)
{
   Active.FreeMode(false);
}
//+------------------------------------------------------------------+
//| Eliminando todas las posiciones restantes                        |
//+------------------------------------------------------------------+
CMtrade::~CMtrade()
{
   Active.FreeMode(true);
   Active.Clear();
}
//+------------------------------------------------------------------+
//| Crea una nueva posición y la añade a la lista de posiciones      |
//| activas.                                                         |
//+------------------------------------------------------------------+
void CMtrade::EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume)
{
   CMposition* pos = CMposition::CreateOnBarOpen(bar, pos_type, volume);
   Active.Add(pos);
}
//+------------------------------------------------------------------+
//| Cierra una posición virtual según el índice pos_index al precio  |
//| de apertura de la barra transmitida                              |
//+------------------------------------------------------------------+
void CMtrade::CloseAtOpenBar(MqlRates &bar, int pos_index)
{
   CMposition* pos = Active.At(pos_index);
   pos.CloseOnBarOpen(bar);
   Active.Delete(pos_index);
   History.Add(pos);
}
//+------------------------------------------------------------------+

 En la práctica, toda su funcionalidad se reduce a dos funciones:

  1. Obtener una nueva posición del método estático CMposition::CreateOnBarOpen y añadirlo a la lista Active (método EntryOnOpenBar);
  2. Pasar la posición elegida de la lista de posiciones activas a la lista de históricas, además, en este caso, la posición reubicada se cierra con el método estático CMposition::CLoseOnBarOpen.

Hemos creado una clase comercial, y ahora tenemos los componentes para realizar la simulación del experto.


Primera simulación del experto. Trabajando en el optimizador

Vamos a reunir todos los componentes juntos. Aquí mostramos el código de una estrategia con dos medias móviles para trabajar en el optimizador matemático. 

//+------------------------------------------------------------------+
//|                                                      MExpert.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <RingBuffer\RiSma.mqh>
#include "Mtester.mqh"

input int PeriodFastMa = 9;
input int PeriodSlowMa = 16;

CRiSMA FastMA;    // Búfer circular para el cálculo de la media móvil rápida
CRiSMA SlowMA;    // Búfer circular para el cálculo de la media móvil lenta
CMtrade Trade;    // Módulo comercial para los cálculos virtuales

//+------------------------------------------------------------------+
//| Cotizaciones con las que va a tener lugar la simulación          |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //-- Comprobamos que la combinación de los parámetros sea correcta
   //-- La media rápida no puede ser mayor que la lenta
   //if(PeriodFastMa >= PeriodSlowMa)
   //   return INIT_PARAMETERS_INCORRECT;
   //-- Inicializamos los periodos de los búferes circulares
   FastMA.SetMaxTotal(PeriodFastMa);
   SlowMA.SetMaxTotal(PeriodSlowMa);
   //-- Cargamos las cotizaciones para el símbolo indicado.
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("No se han encontrado las cotizaciones para el símbolo " + Symbol() + ". Cree el recurso de cotizaciones correspondiente.");
      return INIT_FAILED;
   }
   printf("Se han cargado " + (string)ArraySize(Rates) + " barras del símbolo " + Symbol());
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Describimos la estrategia                                        |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates)-1; i++)
   {
      MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      //-- Cerramos las posiciones opuestas con respecto a la señal actual
      for(int k = Trade.Active.Total()-1; k >= 0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+1], k);   
      }
      //-- Si no hay posiciones, abrimos una con la dirección establecida.
      if(Trade.Active.Total() == 0)
         Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0);
   }
   double profit = 0.0;
   for(int i = 0; i < Trade.History.Total(); i++)
   {
      CMposition* pos = Trade.History.At(i);
      profit += pos.Profit();
   }
   return profit;
}
//+------------------------------------------------------------------+

Ahora la función OnTester ha sido rellenada por completo. El código se ha construido de forma extremadamente sencilla. Vamos a describir su funcionamiento por puntos.

  1. Se iteran todas las cotizaciones en el ciclo for.
  2. Dentro del ciclo se define la dirección actual de la transacción: compra, si la SMA rápida está por encima de la lenta, y lenta, en el caso opuesto.
  3. Se iteran todas las transacciones activas, y si su dirección no coincide con la dirección actual, se cierran.
  4. Si no hay posición, se abre una nueva posición en la dirección establecida.
  5. Al final de la iteración, se iteran de nuevo todas las posiciones cerradas y se calcula el beneficio total de las mismas, que retorna el simulador de estrategias.

Nuestro experto está listo para la simulación en el optimizador. Simplemente lo iniciaremos en el modo de cálculos matemáticos. Para comprobar que la optimización funciona, vamos a hacer la iteración completa de los parámetros de la media móvil como se muestra en la figura de abajo: 

Fig. 5. Eligiendo el campo de parámetros optimizado

En nuestro ejemplo hay 1000 pasadas de optimización, cada una de las cuales procesa 1 año de historia de minutos. Sin embargo, en este modo, el cálculo no ocupa tanto tiempo. En una computadora con un procesador i7, la optimización completa ha ocupado 1 minuto aproximadamente, después de lo cual, se ha construido el gráfico:

Fig. 6. Gráfico de 1000 pasadas en el modo "optimización lenta".

Pero por ahora tenemos muy pocos instrumentos que nos permitan analizar los resultados obtenidos. En esencia, todo lo que tenemos ahora es una cifra única que representa el beneficio virtual. Para corregir esta situación, tenemos que desarrollar nuestro propio formato de datos de optimización y pensar un mecanismo para generarlo y cargarlo. Hablaremos de ello más abajo.

Almacenamiento de los resultados de optimización personalizados con la ayuda de un mecanismo de frames

En MetaTrader 5 se ha implementado una técnica muy avanzada para trabajar con datos personalizados. Se basa en el mecanismo de generación y obtención de los llamados frames. Se trata, en esencia, de datos binarios normales, ubicados o bien como valores aparte, o bien como una matriz con estos valores. Por ejemplo, en el momento de optimización en la función OnTester, podemos generar una matriz de datos de tamaño aleatorio y enviarlos al simulador de estrategias de MetaTrader 5. Los datos que se contienen en esta matriz pueden leerse con la ayuda de la función FrameNext, y también ser procesados posteriormente, por ejemplo, mostrándolos en la pantalla. El propio trabajo con los frames es posible solo en el modo de optimización y solo dentro de tres funciones: OnTesterInit(), OnTesterDeinit() y OnTesterPass(). Ninguna de ellas tiene parámetros, ni retorna valor alguno. Pero todo es más sencillo de lo que pueda parecer. Para ilustrar lo mencionado, escribiremos un sencillo script que muestra el algoritmo general de trabajo con los frames:

//+------------------------------------------------------------------+
//|                                               OnTesterSample.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input int Param = 1;
//+------------------------------------------------------------------+
//| OnTesterInit function                                            |
//+------------------------------------------------------------------+
void OnTesterInit()
{
   printf("Comenzamos la optimización");      
}
//+------------------------------------------------------------------+
//| Aquí tiene lugar la pasada de la estrategia                      |
//+------------------------------------------------------------------+
double OnTester()
{
   uchar data[5] = {1,2,3,4,5};        // Generamos los datos para nuestro frame
   FrameAdd("sample", 1, Param, data); // Creamos un nuevo frame con nuestros datos
   return 3.0;
}
//+------------------------------------------------------------------+
//| Aquí podemos obtener el último frame añadido de la optimización  |
//+------------------------------------------------------------------+
void OnTesterPass()
{
   ulong pass = 0;
   string name = "";
   ulong id = 0;
   double value = 0.0;
   uchar data[];
   FrameNext(pass, name, id, value, data);
   //-- formamos un archivo de pasada y lo añadimos a un archivo zip
   printf("Obtenido el nuevo frame # " + (string)pass + ". Nombre " + (string)name + " ID: " + (string)id + " Valor del parámetro Param: " + DoubleToString(value, 0));
}
//+------------------------------------------------------------------+
//| OnTesterDeinit function                                          |
//+------------------------------------------------------------------+
void OnTesterDeinit()
{
   printf("Finalizamos la optimización");
}
//+------------------------------------------------------------------+

Iniciamos este código en el simulador de estrategias, eligiendo el modo de cálculos matemáticos. Establecemos el modo de optimización lenta. El parámetro único Param variará entre 10 y 90, con un salto de 10.

Después del inicio de la optimización, comenzarán a llegar mensajes informándonos sobre la obtención de nuevos frames. El comienzo de la optimización y su finalización también se monitorizan a través de los eventos especiales. Logs del funcionamiento de la aplicación:

2017.12.19 16:58:08.101 OnTesterSample (EURUSD,M15)     Comenzamos la optimización
2017.12.19 16:58:08.389 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 1. Nombre de sample ID: 1 Valor del parámetro Param: 20
2017.12.19 16:58:08.396 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 0. Nombre de sample ID: 1 Valor del parámetro Param: 20
2017.12.19 16:58:08.408 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 4. Nombre de sample ID: 1 Valor del parámetro Param: 50
2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 5. Nombre de sample ID: 1 Valor del parámetro Param: 60
2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 2. Nombre de sample ID: 1 Valor del parámetro Param: 30
2017.12.19 16:58:08.432 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 3. Nombre de sample ID: 1 Valor del parámetro Param: 40
2017.12.19 16:58:08.443 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 6. Nombre de sample ID: 1 Valor del parámetro Param: 70
2017.12.19 16:58:08.444 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 7. Nombre de sample ID: 1 Valor del parámetro Param: 80
2017.12.19 16:58:08.450 OnTesterSample (EURUSD,M15)     Obtenido un nuevo frame # 8. Nombre de sample ID: 1 Valor del parámetro Param: 90
2017.12.19 16:58:08.794 OnTesterSample (EURUSD,M15)     Finalizamos la optimización

A nosotros nos interesan más los mensajes que muestran la información sobre el número de frame, su identificador y los valores del parámetro Param. Toda esta valiosa información se puede averiguar con la ayuda de la función FrameNext. 

Una peculiaridad interesante de este modo es el inicio doble del experto. El experto en cuyo código se encuentran los datos del procesador de eventos se inicia dos veces: la primera vez en el optimizador de estrategias, la segunda, en el gráfico, en el modo de tiempo real. Mientras que el experto en el optimizador genera nuevos datos, el experto cargado en el gráfico los recibe. De esta forma, los códigos fuente del experto, aunque se encuentran en un mismo lugar, son precesados por diferentes ejemplares del experto. 

Tras obtener los datos en la función OnTesterPass, los podemos procesar de cualquier forma. En el ejemplo del simulador, estos datos se muestran en la consola con la ayuda de la función printf. Pero el procesamiento de datos que nosotros tenemos que organizar puede ser bastante más complejo. Precisamente sobre ello hablaremos en el próximo apartado.


Obteniendo la representación de bytes de la historia de posiciones. Guardando los datos en frames

El mecanismo de frames nos ofrece un método de almacenamiento, procesamiento y distribución de la información. Sin embargo, necesitaremos generar propiamente esta información. En el ejemplo de más arriba tenemos la sencilla matriz estática uchar, con los valores 1, 2, 3, 4, 5. La utilidad de estos datos es escasa. Y es que una matriz de bytes puede tener una longitud aleatoria y guardar cualquier dato. Para ello, deberemos convertir los tipos personalizados de datos en una matriz de bytes del tipo uchar. Ya hicimos algo semejante con MqlRates, cuando guardamos las cotizaciones en una matriz de bytes. Así haremos también en el caso de nuestros datos.

El simulador de estrategias personalizado consta de forma condicional de dos partes. La primera parte genera los datos, la segunda los analiza y representa con un aspecto cómodo para el usuario. También es obvio que la información básica para el análisis de la estrategia se puede obtener analizando todas las transacciones históricas. Por ello, al final de cada pasada convertiremos todas las transacciones históricas en una matriz de bytes, y luego la añadiremos al nuevo marco. Una vez obtenido el nuevo frame en la función OnTesterPass(), podemos añadirlo al anteriormente obtenido, creando una colección completa de frames. 

No solo tendremos que convertir los datos sobre las posiciones en una matriz de bytes, también tendremos que descargarlos. Para ello, hay que escribir dos procedimiento para cada tipo de datos: 

  • Procedimiento de conversión del tipo personalizado en una matriz de bytes;
  • Procedimiento de conversión de una matriz de bytes en un tipo personalizado.

Ya hemos escrito el módulo comercial CMtrade con dos colecciones de posiciones: activas e históricas. Vamos a concentrarnos solo en las posiciones históricas. Los procedimientos de las posiciones virtuales los escribiremos con la forma de los métodos correspondientes.

Método que convierte la posición en una matriz de datos:

//+-----------------------------------------------------------------------------+
//| Convierte la posición en una representación de bytes en forma de matriz     |
//+-----------------------------------------------------------------------------+
int CMposition::ToCharArray(int dst_start, uchar &array[])
{
   int offset = dst_start;
   //-- Copy time open position
   type2char.time_value = m_time_open;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime));
   offset += sizeof(datetime);
   //-- Copy time close position
   type2char.time_value = m_time_close;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime));
   offset += sizeof(datetime);
   //-- Copy price open position
   type2char.double_value = m_price_open;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //-- Copy price close position
   type2char.double_value = m_price_close;  
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //-- Copy volume position
   type2char.double_value = m_volume; 
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //-- Copy spread symbol
   type2char.int_value = m_spread;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(int));
   offset += sizeof(int);
   //-- Copy type of position
   type2char.int_value = m_type;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(char));
   offset += sizeof(int);
   //-- return last offset
   return offset;
}

 Procedimiento inverso:

//+------------------------------------------------------------------+
//| Carga la posición desde una matriz de bytes                      |
//+------------------------------------------------------------------+
int CMposition::FromCharArray(int dst_start, uchar &array[])
{
   int offset = dst_start;
   //-- Copy time open position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime));
   m_time_open = type2char.time_value;
   offset += sizeof(datetime);
   //-- Copy time close position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime));
   m_time_close = type2char.time_value;
   offset += sizeof(datetime);
   //-- Copy price open position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_price_open = type2char.double_value;
   offset += sizeof(double);
   //-- Copy price close position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_price_close = type2char.double_value;
   offset += sizeof(double);
   //-- Copy volume position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_volume = type2char.double_value;
   offset += sizeof(double);
   //-- Copy spread symbol
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int));
   m_spread = type2char.int_value;
   offset += sizeof(int);
   //-- Copy type of position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int));
   m_type = (ENUM_POSITION_TYPE)type2char.int_value;
   offset += sizeof(int);
   //-- return last offset
   return offset;
}

El núcleo de ambos procedimientos es la combinación TypeToChar (se usa su ejemplar type2char):

//+------------------------------------------------------------------+
//| Conversión de tipos sencillos en una matriz de bytes             |
//+------------------------------------------------------------------+
union TypeToChar
{
   uchar    char_array[128];
   int      int_value;
   double   double_value;
   float    float_value;
   long     long_value;
   short    short_value;
   bool     bool_value;
   datetime time_value;
   char     char_value;
};

Todo está construido de forma análoga a la combinación RateToByte, analizada en el apartado sobre la conversión de cotizaciones. 

Ambos procedimientos se han construido de tal forma que permiten cargar los datos desde una matriz global, que contiene los datos sobre todas las posiciones virtuales cerradas. Esto permite organizar un algoritmo de iteración efectivo, que no requiera copiado adicional de memoria.

La propia iteración de todas las posiciones históricas se realizará en la clase CMTrade. Esto es lógico, teniendo en cuenta que en ella se almacena la colección de posiciones históricas. La clase, al igual que CMposition, funciona en dos direcciones: permite convertir la colección de posiciones históricas en una matriz uchar, y también ejecutar el procedimiento inverso, a saber, cargar una lista de posiciones históricas desde la matriz de bytes.

Procedimiento de conversión de la colección en una matriz de bytes:

//+--------------------------------------------------------------------------------+
//| Convierte la lista de posiciones históricas en un archivo zip comprimido en    |
//| forma de matriz de bytes. Retorna true en caso de éxito                        |
//| y false en caso contrario.                                                     |
//+--------------------------------------------------------------------------------+
bool CMtrade::ToCharArray(uchar &array[])
{
   int total_size = CMposition::Sizeof()*History.Total();
   if(total_size == 0)
   {
      printf(__FUNCTION__ +  ": Received  array is empty");
      return false;
   }
   if(ArraySize(array) != total_size && ArrayResize(array, total_size) != total_size)
   {
      printf(__FUNCTION__ +  ": failed resized received array");
      return false;
   }
   //-- Guadamos las posiciones en un flujo de bytes
   for(int offset = 0, i = 0; offset < total_size; i++)
   {
      CMposition* pos = History.At(i);
      offset = pos.ToCharArray(offset, array);
   }
   return true;
}

Procedimiento inverso:

//+------------------------------------------------------------------------------+
//| Carga la lista de posiciones históricas desde un archivo zip comprimido      |
//| transmitido en forma de matriz de bytes. Retorna true en caso de             |
//| éxito, y false en caso contrario.                                            |
//+------------------------------------------------------------------------------+
bool CMtrade::FromCharArray(uchar &array[], bool erase_prev_pos = true)
{
   if(ArraySize(array) == 0)
   {
      printf(__FUNCTION__ +  ": Received  array is empty");
      return false;
   }
   //-- El tamaño del flujo de bytes debe coincidir exactamente con la representación de bytes de las posiciones
   int pos_total = ArraySize(array)/CMposition::Sizeof();
   if(ArraySize(array)%CMposition::Sizeof() != 0)
   {
      printf(__FUNCTION__ +  ": Wrong size of received  array");
      return false;
   }
   if(erase_prev_pos)
      History.Clear();
   //-- Restauramos todas las posiciones desde el flujo de bytes
   for(int offset = 0; offset < ArraySize(array);)
   {
      CMposition* pos = new CMposition();
      offset = pos.FromCharArray(offset, array);
      History.Add(pos);
   }
   return History.Total() > 0;
}

Para reunir todos los elementos juntos, bastará con obtener la representación de bytes de las posiciones históricas al final de la pasada y almacenarlas en un frame:

//+------------------------------------------------------------------+
//| Describimos la estrategia                                        |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates)-1; i++)
   {
      MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      //-- Cerramos las posiciones opuestas con respecto a la señal actual
      for(int k = Trade.Active.Total()-1; k >= 0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+1], k);   
      }
      //-- Si no hay posiciones, abrimos una con la dirección establecida.
      if(Trade.Active.Total() == 0)
         Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0);
   }
   uchar array[];
   //-- Obtenemos la representación de bytes de las posiciones históricas
   Trade.ToCharArray(array); 
   //-- Cargamos la representación de bytes en el frame y la transmitimos para su procesamiento posterior
   FrameAdd(MTESTER_STR, MTESTER_ID, 0.0, array);  
   return Trade.History.Total();
}

Después de que el frame se haya formado y haya sido enviado al procedimiento de procesamiento de OnTesterPass(), será necesario pensar en lo que vamos a hacer con él después. Ya hemos mencionado que nuestro simulador de estrategias consta de dos partes: el bloque de generación de datos y el bloque de análisis de la información recopilada. Para este tipo de análisis es necesario guardar todos los frames generados en un formato cómodo y ahorrativo, para que más tarde sea igual de cómodo analizar este formato. Para ello, podemos usar un archivo zip. En primer lugar, este comprime los datos efectivamente, y por lo tanto, la información de incluso miles de transacciones ocupará poco espacio. En segundo lugar, proporciona un sistema de archivos conveniente. Cada pasada se puede almacenar en forma de archivo aparte dentro de un solo archivo ZIP.

Bien, vamos a escribir el procedimiento de conversión del contenido de bytes del frame a un archivo ZIP.

//+------------------------------------------------------------------+
//| Añadimos cada nueva pasada al archivo ZIP                        |
//+------------------------------------------------------------------+
void OnTesterPass()
{
   ulong pass = 0;
   string name = "";
   ulong id = 0;
   double value = 0.0;
   uchar data[];
   FrameNext(pass, name, id, value, data);
   //-- formamos un archivo de pasada y lo añadimos a un archivo zip
   printf("Obtenido un nuevo frame con un tamaño de " + (string)ArraySize(data));
   string file_name = name + "_" + (string)id + "_" + (string)pass + "_" + DoubleToString(value, 5)+".hps";
   CZipFile* zip_file = new CZipFile(file_name, data);
   Zip.AddFile(zip_file);
}

Gracias a que la clase para el trabajo con archivos zip es lo suficientemente potente, y también a que posee métodos universales, resulta muy simple agregar al fichero una nueva pasada en forma de archivo aparte. En esencia, en OnTesterPass se añade un archivo zip al fichero Zip declarado a nivel global:

CZip     Zip;     // fichero Zip que vamos a rellenar con pasadas de optimización

Este proceso se llama de forma paralela al finalizar cada pasada de optimización, y no requiere de grandes recursos computacionales.

Después de finalizar la optimización, el conjunto de archivos zip creado se debe guardar en forma de archivo zip correspondiente. Hacer esto también es muy sencillo. Esto se realiza en el procedimiento OnTesterDeinit():

//+--------------------------------------------------------------------------------------+
//| Guardamos el fichero zip de todas las pasadas en el disco duro de la computadora     |
//+--------------------------------------------------------------------------------------+
void OnTesterDeinit()
{
   Zip.SaveZipToFile(OptimizationFile, FILE_COMMON);
   string f_totals = (string)Zip.TotalElements();
   printf("La optimización ha finalizado. Total de pasadas de optimización guardadas: " + f_totals);
}

Aquí, OptimizationFile es un parámetro de línea personalizado, que establece el nombre de la optimización. Por defecto, es igual a "Optimization.zip". De esta forma, después de finalizar la optimización de nuestra estrategia personalizada SmaSample, se guardará el fichero zip correspondiente. Lo encontraremos en la carpeta Files y lo abriremos con los recursos estándar:

Fig. 7. Contenido del archivo de optimización

Como podemos ver, todas las pasadas guardadas se almacenan estupendamente, mostrando un alto nivel de compresión, de 3 a 5 veces. 

Al reunir y almacenar los datos en el disco duro, tenemos que cargarlos en otro programa y analizarlos. Nos ocuparemos de esto en la siguiente sección.


Creando un analizador de estrategias

En la sección anterior hemos creado un fichero zip que incluye información sobre todas las pasadas. Ahora tenemos que procesar esta información. Para ello, crearemos especialmente el programa M-Tester Analyzer. Este cargará el fichero generado y representará cada pasada en forma de cómodo gráfico de balance. Asimismo, M-Tester Analyzer calculará la estadística reunida para la pasada elegida.

Una de las peculiaridades clave del complejo de simulación que hemos creado es la posibilidad de almacenar información sobre todas las pasadas al mismo tiempo. Esto significa que solo podemos realizar la optimización una vez. Todos sus pasadas se almacenarán en un archivo y se transmitirán al usuario. A continuación, podremos descargar cualquier pasada de esta optimización y ver sus estadísticas, sin perder tiempo en una nueva llamada al simulador de estrategias.

Secuencia de acciones del analizador:

  1. Carga del fichero de optimización elegido
  2. Elección de una de las pasadas de optimización en este fichero
  3. Construcción del gráfico de dinámica de balance, basado en las transacciones disponibles
  4. Cálculo de la estadística principal de la pasada, que incluye parámetros tales como el número de transacciones, el beneficio total, las pérdidas totales, el factor de beneficio, la esperanza matemática, etcétera.
  5. Muestra de la estadística calculada en forma de recuadro en la ventana principal del programa.

Es necesario proporcionar al usuario herramientas para seleccionar una pasada al azar del archivo: posibilitaremos una fácil transición de la ejecución actual a la siguiente o la anterior, dándole la oportunidad de indicar cualquier número de pasada.

El programa se basará en el motor gráfico CPanel. Hasta la fecha no existe un artículo específico sobre esta biblioteca, pero resulta fácil de comprender, es compacta y en repetidas ocasiones se ha utilizado en diversos proyectos y artículos.

El código principal de nuestro analizador se ubica en la clase CAnalizePanel, derivada de CElChart. El propio analizador se ha ejecutado en forma de experto. El archivo principal del experto inicia la ventana gráfica del analizador. Aquí tenemos el archivo principal del experto:

//+------------------------------------------------------------------+
//|                                                    mAnalizer.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "mAnalizerPanel.mqh"
CAnalyzePanel Panel;
input string FilePasses = "Optimization.zip";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   Panel.Width(800);
   Panel.Height(630);
   Panel.XCoord(10);
   Panel.YCoord(20);
   Panel.LoadFilePasses(FilePasses);
   Panel.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Panel.Hide();
}

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   switch(id)
   {
      case CHARTEVENT_OBJECT_ENDEDIT:
      {
         CEventChartEndEdit event(sparam);
         Panel.Event(&event);
         break;
      }
      case CHARTEVENT_OBJECT_CLICK:
      {
         CEventChartObjClick event(sparam);
         Panel.Event(&event);
         break;
      }
   }
   ChartRedraw();
}
//+------------------------------------------------------------------+

Como podemos ver, el código es extremadamente sencillo. Se crea un objeto del tipo CAnalyzePanel. A continuación, en el método OnInit se establecen sus dimensiones, después de lo cual se representa en el gráfico actual (método Show). De todos los eventos que llegan al gráfico, nos interesan solo dos: la finalización de la introducción del texto y la pulsación del objeto gráfico. Estos eventos se convierten en un objeto especial del tipo CEvent y son transmitidos a nuestro panel (Panel.Event(...)). El panel del analizador recibe estos eventos y los representa.

Vamos a pasar a la descripción del propio panel del analizador. El panel consta de la enorme clase CAnalyzePanel, por eso no vamos a publicar su contenido aquí por completo. Quien así lo desee, podrá analizar el código completo en los anexos al artículo. Vamos a limitarnos solo a una breve descripción de su funcionamiento. Para ello, mostraremos el prototipo de la clase:

//+------------------------------------------------------------------+
//|                                                    mAnalizer.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Panel\ElChart.mqh>
#include <Panel\ElButton.mqh>
#include <Graphics\Graphic.mqh>
#include "ListPass.mqh"
#include "TradeAnalyze.mqh"
//+------------------------------------------------------------------+
//| Panel del analizador de pasadas del analizador matemático        |
//+------------------------------------------------------------------+
class CAnalizePanel : public CElChart
{
private:
   //-- Matrices de los elementos y estadística de los mismos
   CArrayObj      m_stat_descr;     // Descripción de las estadísticas
   CArrayObj      m_stat_all;       // Valor de las estadísticas para todas las transacciones
   CArrayObj      m_stat_long;      // Valor de las estadísticas para las transacciones largas
   CArrayObj      m_stat_short;     // Valor de las estadísticas para las transacciones cortas
   CTradeAnalize  m_analize;        // Módulo de cálculo de estadísticas
   //-- Elementos gráficos
   CElChart       m_name_analyze;   // Nombre de la ventana principal
   CElChart       m_np;             // Rótulo "Pass #"
   CElChart       m_of_pass;        // Rótulo "of ### passes"
   CElChart       m_pass_index;     // Ventana de muestra del número de pasada
   CElButton      m_btn_next;       // Botón "próxima pasada"
   CElButton      m_btn_prev;       // Botón "pasada anterior"
   CGraphic       m_graphic;        // Gráfico de la dinámica de balance
   //-- Infraestructura
   CListPass      m_passes;         // Lista de pasadas
   int            m_curr_pass;      // Índice de la pasada actual
   CCurve*        m_balance_hist;   // Línea de la dinámica de balance en el gráfico
   bool           IsEndEditPass(CEvent* event);
   bool           IsClick(CEvent* event, CElChart* el);
   void           NextPass(void);
   void           PrevPass(void);
   int            GetCorrectPass(string text);
   void           RedrawGraphic(void);
   void           RedrawCurrPass(void);
   void           PlotStatistic(void);
   string         TypeStatToString(ENUM_MSTAT_TYPE type);
   void           CreateStatElements(void);
   string         ValueToString(double value, ENUM_MSTAT_TYPE type);
public:
                  CAnalizePanel(void);
   bool           LoadFilePasses(string file_name, int file_common = FILE_COMMON);
   virtual void   OnShow();
   virtual void   OnHide();
   virtual void   Event(CEvent *event);
};

Como podemos ver, el funcionamiento principal de esta clase se esconde dentro, y entre los métodos públicos, el principal es solo la carga del fichero zip que contiene las pasadas de optimización. Todo el funcionamiento de la clase se puede dividir de forma condicional en tres partes:

  1. La creación de un gráfico y la adición de una línea de balance al propio gráfico.
  2. La creación de marcas de texto en forma de elementos CElChart, que representan la estadística de la simulación.
  3. El propio cálculo de las estadísticas de la pasada.

Vamos a describir brevemente cada uno de estos apartados. 

Hay que crear suficientes elementos para representar toda la estadística reunida de cada pasada. Nuestro analizador representa los diez parámetros estadísticos principales. Además, cada parámetro se calcula por separado para todas las transacciones: solo para las compras y solo para las ventas. Aparte, también necesitamos 10 marcas de texto que representen los nombres de los indicadores. De esta forma, necesitaremos crear 40 marcas de texto. Para no crear cada elemento manualmente, escribiremos un procedimiento de automatización. Para ello, a cada parámetro estadístico definido se le da su propio identificador en una enumeración especial:

//+------------------------------------------------------------------+
//| Identificadores del tipo de valor estadístico                    |
//+------------------------------------------------------------------+
enum ENUM_MSTAT_TYPE
{
   MSTAT_PROFIT,
   MSTAT_ALL_WINS_MONEY,
   MSTAT_ALL_LOSS_MONEY,
   MSTAT_TRADERS_TOTAL,
   MSTAT_WIN_TRADERS,
   MSTAT_LOSS_TRADERS,   
   MSTAT_MAX_PROFIT,
   MSTAT_MAX_LOSS,
   MSTAT_PROFIT_FACTOR,
   MSTAT_MATH_EXP,   
};
#define MSTAT_ELEMENTS_TOTAL 10

Asimismo, definimos el identificador para la dirección del cálculo:

/+-------------------------------------------------------------------+
//| La estadística puede calcularse como una de tres direcciones     |
//+------------------------------------------------------------------+
enum ENUM_MSTATE_DIRECT
{
   MSTATE_DIRECT_ALL,      // Para todas las transacciones
   MSTATE_DIRECT_LONG,     // Solo para las compras
   MSTATE_DIRECT_SHORT,    // Solo para las ventas
};

El panel contiene cuatro grupos de elementos, cada uno de los cuales se encuentra en su propia matriz:

  • Elementos que representan el nombre de la estadística (matriz m_stat_descr)
  • Elementos que representan los valores de las estadísticas para todas las estrategias (matriz m_stat_all)
  • Elementos que representan los valores de las estadísticas para las transacciones largas (matriz m_stat_long)
  • Elementos que representan los valores de las estadísticas para las transacciones cortas (matriz m_stat_short)

Todos los elementos se crean en el momento del primer inicio en el método CAnalyzePanel::CreateStatElements(void).

Después de que los elementos hayan sido creados, es necesario rellenarlos con los valores correctos. El cálculo de estos valores se delega en la clase externa CTradeAnalize:

#include <Arrays\ArrayObj.mqh>
#include <Dictionary.mqh>
#include "..\MTester\Mposition.mqh"
//+------------------------------------------------------------------+
//| Elemento auxiliar que contiene los campos necesarios             |
//+------------------------------------------------------------------+
class CDiffValues : public CObject
{
public:
   double all;
   double sell;
   double buy;
   CDiffValues(void) : all(0), buy(0), sell(0)
   {
   }
};
//+------------------------------------------------------------------+
//| Clase de análisis estadístico                                    |
//+------------------------------------------------------------------+
class CTradeAnalize
{
private:
   CDictionary m_values;
   
public:
   void     CalculateValues(CArrayObj* history);
   double   GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct);
};
//+------------------------------------------------------------------+
//| Calcula el valor de la estadística                               |
//+------------------------------------------------------------------+
double CTradeAnalize::GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct)
{
   CDiffValues* value = m_values.GetObjectByKey(type);
   switch(direct)
   {
      case MSTATE_DIRECT_ALL:
         return value.all;
      case MSTATE_DIRECT_LONG:
         return value.buy;
      case MSTATE_DIRECT_SHORT:
         return value.sell;
   }
   return EMPTY_VALUE;
}
//+------------------------------------------------------------------+
//| Calcula el número de transacciones para cada dirección           |
//+------------------------------------------------------------------+
void CTradeAnalize::CalculateValues(CArrayObj *history)
{
   m_values.Clear();
   for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++)
      m_values.AddObject(i, new CDiffValues());
   CDiffValues* profit = m_values.GetObjectByKey(MSTAT_PROFIT);
   CDiffValues* wins_money = m_values.GetObjectByKey(MSTAT_ALL_WINS_MONEY);
   CDiffValues* loss_money = m_values.GetObjectByKey(MSTAT_ALL_LOSS_MONEY);
   CDiffValues* total_traders = m_values.GetObjectByKey(MSTAT_TRADERS_TOTAL);
   CDiffValues* win_traders = m_values.GetObjectByKey(MSTAT_WIN_TRADERS);
   CDiffValues* loss_traders = m_values.GetObjectByKey(MSTAT_LOSS_TRADERS);
   CDiffValues* max_profit = m_values.GetObjectByKey(MSTAT_MAX_PROFIT);
   CDiffValues* max_loss = m_values.GetObjectByKey(MSTAT_MAX_LOSS);
   CDiffValues* pf = m_values.GetObjectByKey(MSTAT_PROFIT_FACTOR);
   CDiffValues* mexp = m_values.GetObjectByKey(MSTAT_MATH_EXP);
   total_traders.all = history.Total();
   for(int i = 0; i < history.Total(); i++)
   {
      CMposition* pos = history.At(i);
      profit.all += pos.Profit();
      if(pos.PositionType() == POSITION_TYPE_BUY)
      {
         if(pos.Profit() > 0)
         {
            win_traders.buy++;
            wins_money.buy += pos.Profit();
         }
         else
         {
            loss_traders.buy++;
            loss_money.buy += pos.Profit();
         }
         total_traders.buy++;
         profit.buy += pos.Profit();
      }
      else
      {
         if(pos.Profit() > 0)
         {
            win_traders.sell++;
            wins_money.sell += pos.Profit();
         }
         else
         {
            loss_traders.sell++;
            loss_money.sell += pos.Profit();
         }
         total_traders.sell++;
         profit.sell += pos.Profit();
      }
      if(pos.Profit() > 0)
      {
         win_traders.all++;
         wins_money.all += pos.Profit();
      }
      else
      {
         loss_traders.all++;
         loss_money.all += pos.Profit();
      }
      if(pos.Profit() > 0 && max_profit.all < pos.Profit())
         max_profit.all = pos.Profit();
      if(pos.Profit() < 0 && max_loss.all > pos.Profit())
         max_loss.all = pos.Profit();
   }
   mexp.all = profit.all/total_traders.all;
   mexp.buy = profit.buy/total_traders.buy;
   mexp.sell = profit.sell/total_traders.sell;
   pf.all = wins_money.all/loss_money.all;
   pf.buy = wins_money.buy/loss_money.buy;
   pf.sell = wins_money.sell/loss_money.sell;
}

Del cálculo propiamente dicho, se encarga el método CalculateValues. Para que funcione, debemos transmitir la matriz CArrayObj, que contiene los elementos CMposition. Pero, ¿de dónde sale la propia matriz con estas posiciones virtuales?

El tema es que en la clase CAnalyzePanel existe otra clase — CListPass. Precisamente esta clase carga el fichero zip y crea la colección de pasadas. Esta clase se ha construido de forma muy sencilla:

//+------------------------------------------------------------------+
//|                                                    Optimazer.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Zip\Zip.mqh>
#include <Dictionary.mqh>
#include "..\MTester\MTrade.mqh"
//+------------------------------------------------------------------+
//| Almacena la lista de optimizaciones                              |
//+------------------------------------------------------------------+
class CListPass
{
private:
   CZip        m_zip_passes;  // Archivo de todas las pasadas de las optimizaciones
   CDictionary m_passes;      // Posiciones históricas ya cargadas   
public:
   bool        LoadOptimazeFile(string file_name, int file_common = FILE_COMMON);
   int         PassTotal(void);
   CArrayObj*  PassAt(int index);
};
//+------------------------------------------------------------------+
//| Carga la lista de optimizaciones desde un archivo zip            |
//+------------------------------------------------------------------+
bool CListPass::LoadOptimazeFile(string file_name,int file_common=FILE_COMMON)
{
   m_zip_passes.Clear();
   if(!m_zip_passes.LoadZipFromFile(file_name, file_common))
   {     
      printf("Failed load optimization file. Last Error");
      return false;
   }
   return true;
}
//+------------------------------------------------------------------+
//| Número de pasadas                                                |
//+------------------------------------------------------------------+
int CListPass::PassTotal(void)
{
   return m_zip_passes.TotalElements();
}
//+----------------------------------------------------------------------+
//| Retorna la lista de transacciones de la pasada con el número index   |
//+----------------------------------------------------------------------+
CArrayObj* CListPass::PassAt(int index)
{
   if(!m_passes.ContainsKey(index))
   {
      CZipFile* zip_file = m_zip_passes.ElementAt(index);
      uchar array[];
      zip_file.GetUnpackFile(array);
      CMtrade* trade = new CMtrade();
      trade.FromCharArray(array);
      m_passes.AddObject(index, trade);
   }
   CMtrade* trade = m_passes.GetObjectByKey(index);
   //printf("Total Traders: " + (string)trade.History.Total());
   return &trade.History;
}

Como podemos ver, la clase CListPass carga el archivo de optimizaciones, pero no lo descomprime. Esto significa que incluso en la memoria de la computadora, todos los datos de optimización utilizados se guardan comprimidos, gracias a lo cual se ahorra memoria operativa de la propia computadora. La pasada solicitada se descomprime y transforma en CMtrade, después de lo cual se guarda ya dentro del espacio interno de forma descomprimida. Al recurrir a este elemento la próxima vez, no será necesario descomprimirlo de nuevo.

De nuevo recurriremos a la clase CAnalyzePanel. Ya hemos aclarado desde dónde se cargan las posiciones (clase CListPass) y cómo se calcula su estadística (clase CTradeAnalyze). Después de crear los elementos gráficos, solo queda rellenarlos con los valores necesarios. De esto se encargará el método CAnalyzePanel::PlotStatistic(void):

//+------------------------------------------------------------------+
//| Representa la estadística                                        |
//+------------------------------------------------------------------+
void CAnalyzePanel::PlotStatistic(void)
{
   if(m_stat_descr.Total() == 0)
      CreateStatElements();
   CArrayObj* history = m_passes.PassAt(m_curr_pass-1);
   m_analize.CalculateValues(history);
   for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++)
   {
      ENUM_MSTAT_TYPE stat_type = (ENUM_MSTAT_TYPE)i;
      //-- all traders
      CElChart* el = m_stat_all.At(i);
      string v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_ALL), stat_type);
      el.Text(v);
      //-- long traders
      el = m_stat_long.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_LONG), stat_type);
      el.Text(v);
      //-- short traders
      el = m_stat_short.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_SHORT), stat_type);
      el.Text(v);
   }
}

Ya hemos analizado todos los elementos principales, imprescindibles para el funcionamiento del panel de nuestro analizador. La descripción ha resultado inconsistente, pero en eso precisamente reside la esencia de la programación: todos los elementos están interconectados entre sí, y a veces todo se debe describir al mismo tiempo.

Bien, ha llegado el momento de iniciar el analizador en el gráfico. Vamos a hacerlo, pero compruebe en primer lugar que el archivo zip de optimización se encuentra en el directorio FILE_COMMON. Por defecto, el analizador carga el archivo "Optimization.zip", y precisamente este debe encontrarse en el directorio general.

El mayor efecto de las características implementadas se puede ver en el momento de la conmutación de las pasadas. El gráfico y las estadísticas se actualizan automáticamente. En la siguiente captura de pantalla se capta este momento:


Fig. 8. Conmutación de las pasadas en el analizador de cálculos matemáticos

Para comprender mejor el funcionamiento del panel, mostraremos su esquema técnico con pistas: los elementos principales de la interfaz han sido destacados con marcos en los que se indican las clases y métodos responsables de cada grupo de elementos:


Fig. 9. Elementos principales de la interfaz

Como conclusión, describiremos la estructura del proyecto que hemos conseguido. Todos los códigos fuente se encuentran en el archivo MTester.zip. El propio proyecto se encuentra en la carpeta MQL5\Experts\MTester. Sin embargo, como sucede con cualquier programa complejo, el proyecto necesita la inclusión de bibliotecas adicionales. Aquellas bibliotecas que no hayan sido incluidas en el paquete estándar de MetaTrader 5, estarán presentes en este archivo, en la carpeta MQL5\Include. En primer lugar, se trata de la biblioteca gráfica CPanel (su ubicación es: MQL5\Include\Panel). También nos referimos a la biblioteca para trabajar con archivos zip (MQL5\Include\Zip) y a la clase para la organización de la matriz asociativa (MQL5\Include\Dictionary). Asimismo, para comodidad del usuario, se han creado dos proyectos MQL5. Es una nueva posibilidad de MetaTrader 5, aparecida hace poco. El primer proyecto se llama MTester y contiene el propio simulador de estrategias y la propia estrategia basada en el cruce de medias móviles (SmaSample.mq5). El segundo proyecto se llama MAnalyzer y contiene el código del panel del analizador.

Aparte de los códigos fuente, el fichero contiene el archivo de optimización Optimization.zip, que contiene cerca de 160 pasadas de la estrategia sobre datos de prueba. Esto ayudará a comprobar la funcionalidad del analizador de pasadas sin necesidad de realizar una nueva simulación. El archivo se ubica en la carpeta MQL5\Files.


Conclusión

Como conclusión, enumeraremos brevemente las tesis de los materiales descritos en el artículo.

  • El simulador de cálculos matemáticos posee una gran velocidad gracias a la ausencia de imitaciones del entorno comercial. Gracias a esto, basándonos en él, podremos escribir nuestro propio algoritmo de alto rendimiento para la simulación de estrategias comerciales sencillas. Sin embargo, debido a la falta de control sobre la corrección de la ejecución de las operaciones comerciales, es posible "mirar hacia el futuro" inadvertidamente, recurriendo a las cotizaciones que aún no han llegado. Los errores en estos "griales" se identifican con bastante dificultad: ese es el precio por disponer de un rendiemiento alto.
  • Encontrándonos en el simulador de cálculos matemáticos, no podemos obtener acceso al entorno comercial. Por consiguiente, tampoco no podremos obtener las cotizaciones del instrumento deseado. De esta forma, en dicho modo usted tendrá que anticiparse y descargar con antelación los datos necesarios y también utilizar sus bibliotecas para el cálculo de los indicadores. En el artículo se ha mostrado cómo preparar los datos, comprimirlos de manera efectiva e incorporarlos al módulo de ejecución del programa. Esta técnica también puede resultar necesaria a aquellos que quieran expandir con su programa datos adicionales para su funcionamiento.
  • Acceso a los indicadores estándar en el modo de cálculos matemáticos. Por eso, tendremos que ejecutar el cálculo de los indicadores necesarios por nuestra cuenta. Sin embargo, la velocidad también es muy importante. Por eso, el cálculo autónomo de los indicadores dentro de los expertos es, no solo la mejor solución, sino también la única en cuanto a velocidad se refiere. Por suerte, la biblioteca de búferes circulares puede ofrecer un cálculo efectivo de los indicadores necesarios en un tiempo constante.
  • El modo de generación de frames en MetaTrader 5 es potente, aunque también es un mecanismo complejo, que da al usuario enormes posibilidades en lo que se refiere a la escritura de sus propios algoritmos de análisis. Por ejmplo, podemos escribir nuestro propio simulador de estrategias, lo cual se ha hecho en este artículo. Para usar plenamente las posibilidades del modo de generación de frames, tenemos que saber trabajar con datos binarios. Gracias precisamente a la capacidad de trabajar con este tipo de datos, se hace posible generar datos de tipo complejo, por ejemplo, una lista de elementos. En el artículo se muestra: cómo crear un tipo personalizado de clases datos (clase de la posición CMPosition); cómo convertirlo en una representación de bytes y añadirlo a un frame; cómo obtener una matriz de bytes a partir de un frame y cómo convertirlas de nuevo en una lista de posiciones personalizada.
  • Una de las partes más importantes del simulador de estrategias es el sistema de guardado de datos. Obviamente, son muchos los datos obtenidos mediante la simulación: cada simulación puede incluir cientos o incluso miles de pasadas, y cada pasada incluye multitud de transacciones, cuyo número puede llegar fácilmente a varias decenas de miles. El éxito de todo el proyecto depende del grado de eficacia con el que se almacene y extienda esta información. Por ello se ha elegido el archivo zip. Gracias a que disponemos para MQL5 de una biblioteca rápida y cómoda para trabajar con este tipo de archivos, se hace muy sencillo organizar nuestro propio lugar de almacenamiento de archivos de las pasadas de optimización. Cada optimización supone un archivo zip que contiene todas las pasadas. Cada pasada está representada por un archivo comprimido. Gracias a la alta compresión de los datos, incluso una optimización de grandes dimensiones se ejecuta en un tamaño bastante modesto.
  • Pero escribir nuestro propio simulador de estrategias no es suficiente. También es necesario un subsistema aparte que analice los resultados de la optimización. Este subsistema ha sido implementado por nosotros utilizando el programa M-Tester Analyzer. Se trata de un módulo de programa aparte, que carga los resultados de la optimización en forma de archivo zip y los muestra en el gráfico, representando también la estadística principal para cada pasada. M-Tester Analyzer se basa en varias clases y en la biblioteca gráfica CPanel. Se trata de una biblioteca sencilla y cómoda, con cuya ayuda se puede construir una potente interfaz gráfica con bastante rapidez. Gracias a la presencia de la biblioteca sistémica CGraphic, el analizador puede mostrar un gráfico informativo de la dinámica del balance. 

A pesar de que hemos obtenido un resultado impresionante, y el simulador de cálculos matemáticos verdaderamente ha funcionado, este carece aún de muchas cosas necesarias. Vamos a enumerar algunos de los componentes que se deberán añadir a las siguientes versiones.

  • Información sobre el símbolo (nombre, coste de un tick, swap y spread, etcétera). Esta información es necesaria para calcular las posibles comisiones, el spread y el swap. También será necesaria para calcular el beneficio en la divisa del depósito (ahora el beneficio se calcula en puntos).
  • Información sobre la estrategia y sus parámetros para cada pasada. Es imprescindible conocer no solo el resultado de la estrategia, sino también el valor de todos sus parámetros. Para ello, en el informe generado también se debe introducir un tipo adicional de datos.
  • Control de la corrección de las acciones producidas. En esta etapa, resulta sencillo "mirar al futuro", gracias a lo cual obtendremos un "grial" sin semejanza alguna con la realidad. En versiones futuras necesitaremos un mecanismo para ejercer un control aunque sea mínimo. Sin embargo, resulta complicado determinar qué aspecto debe tener.
  • Integración del mecanismo de generación del informe con el simulador real de estrategias. Nada impide convertir el resultado obtenido con en el simulador habitual MetaTrader 5 Strategy Tester al formato de informe desarrollado por nosotros. Esto nos dará la posibilidad de analizar los resultados de una simulación fiable con la ayuda de M-Trade Analyzer. De esta forma, tendremos varios sistemas de simulación y un sistema de análisis.
  • Análisis posterior de M-Trade Analayzer. Ahora el programa de análisis posee solo una funcionalidad básica. Es claramente insuficiente para el posterior procesamiento de los datos. Es necesario añadir más estadísticas y gráficos de balance tanto para las ventas, como para las compras. Asimismo, no estaría mal aprender a guardar la historia de transacciones en un archivo de texto y después cargarla, digamos, en Excel.
Ya hemos analizado los aspectos principales de M-Tester, y también sus futuras perspectivas de desarrollo. Si el tema propuesto resulta suficientemente interesante, este artículo tendrá una continuación. Hemos recorrido un largo camino, pero aún queda mucho por hacer. ¡Esperaremos que el momento para una nueva versión de M-Tester llegue muy pronto!

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/4226

Archivos adjuntos |
MTester.zip (12282.19 KB)
LifeHack para tráders: preparando "comida rápida" a partir de indicadores LifeHack para tráders: preparando "comida rápida" a partir de indicadores
Si usted se ha decidido a dar el salto a MQL5 solo ahora, entonces este artículo le resultará muy útil: por una parte, el acceso a los datos de los indicadores y a las series se ha ejecutado en el estilo MQL4, al que usted ya está acostumbrado, y por otro, toda la implementación se ha escrito en MQL5 con la misma sencillez. Todas las funciones son totalmente comprensibles y se adecuan perfectamente a la depuración paso a paso.
Cómo reducir los riesgos del tráder Cómo reducir los riesgos del tráder
El comercio en los mercados financieros se relaciona con una serie de riesgos que deben ser tenidos en cuenta en los algoritmos de los sistemas comerciales. La reducción de dichos riesgos es una tarea vital a la hora de obtener beneficios en el trading.
Gestión de capital según Vince. Implementación como módulo de Wizard MQL5 Gestión de capital según Vince. Implementación como módulo de Wizard MQL5
El artículo se basa en el libro de R.Vince "Las matemáticas de la gestión de capital". En este se analizan los métodos empíricos y paramétricos usados para localizar el tamaño óptimo de lote comercial, sobre cuyas bases se han escrito los módulos comerciales de gestión de capital para el wizard MLQ5.
Creamos una nueva estrategia comercial usando una tecnología de colocación de entradas a los indicadores Creamos una nueva estrategia comercial usando una tecnología de colocación de entradas a los indicadores
En el artículo se expone una tecnología con cuya ayuda cualquiera podrá crear su propia estrategia comercial combinando un conjunto individual de indicadores, y también desarrollar sus propias señales para entrar en el mercado.