Optimización móvil continua (Parte 3): Método de adaptación del robot al optimizador automático

Andrey Azatskiy | 17 marzo, 2020

Introducción

Presentamos el tercer artículo del ciclo dedicado a la creación del optimizador automático para la optimización móvil continua. Podrá leer los demás artículos en los siguientes enlaces:

  1. Optimización móvil continua (Parte 1): Mecanismo de trabajo con los informes de optimización
  2. Optimización móvil continua (Parte 2): Mecanismo de creación de informes de optimización para cualquier robot

El primer artículo del presente ciclo está dedicado a la creación del mecanismo de funcionamiento de los archivos y a la formación de los archivos con el informe de las transacciones que necesita el optimizador automático para ejecutar su trabajo. El segundo artículo habla de los objetos clave que forman la descarga de la historia de transacciones y la creación de los informes de las transacciones sobre la descarga obtenida. El presente artículo actuará como puente entre los dos anteriores, pues en él se analizará el mecanismo de interacción con la DLL descrita en el primer artículo y los objetos para la descarga analizados en el segundo.

Además, se mostrará el proceso de creación de un envoltorio para la clase que se importa desde DLL y se formará un archivo XML con la historia de transacciones, así como un método de interacción con los datos del envoltorio. Además, se describirán dos funciones encargadas de descargar una historia de transacciones general y detallada en un archivo para su posterior estudio. Como conclusión, se presentará una plantilla preparada para la escritura de un robot que podrá trabajar con el optimizador automático. Asimismo, usando como ejemplo un algoritmo estándar del conjunto de expertos por defecto, se mostrará de qué forma se puede mejorar cualquier algoritmo existente para interacturar con el optimizador automático. 

Descargando la historia acumulada de transacciones

A veces, para analizar con mayor detalle la historia o para otras necesidades, se requiere realizar la descarga de la historia de transacciones en un archivo. Por desgracia, semejante interfaz no se ha implementado aún en el terminal, sin embargo, con la ayuda de las clases mencionadas en el artículo anterior, esta tarea también puede ser implementada. En el directorio donde se encuentran los archivos de las clases descritas, también se encuentran dos archivos: "ShortReport.mqh" y "DealsHistory.mqh", que ejecutan igualmente esta tarea, aunque con diferente nivel de detalle.

Vamos a comenzar su análisis por el archivo "ShortReport.mqh". Este archivo contiene una serie de funciones y macrosustituciones, la más importante de ellas es la función "SaveReportToFile", aunque bueno, mejor vayamos por orden. En primer lugar, vamos a ver la función write, que escribe los datos en un archivo. 

//+------------------------------------------------------------------+
//| File writer                                                      |
//+------------------------------------------------------------------+
void writer(string fileName,string headder,string row)
  {
   bool isFile=FileIsExist(fileName,FILE_COMMON); // Bandera de existencia del archivo
   int file_handle=FileOpen(fileName,FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON|FILE_SHARE_WRITE|FILE_SHARE_READ); // Abriendo el archivo
   if(file_handle) // Si se ha abierto el archivo
     {
      FileSeek(file_handle,0,SEEK_END); // Desplazamos el cursor al final del archivo
      if(!isFile) // Si no se trata de un archivo creado recientemente, escribimos el encabezado
         FileWrite(file_handle,headder);
      FileWrite(file_handle,row); // Escribiendo el mensaje
      FileClose(file_handle); // Cerrando el archivo
     }
  }

La escritura tiene lugar en el sandbox de archivos Terminal/Comon/Files. La idea de esta función consiste en realizar la escritura del archivo añadiendo líneas al mismo, por eso, después de abrir el archivo y obtener su manejador, nosotros pasaremos al final del archivo. Si el archivo acaba de ser creado, escribiremos los encabezados transmitidos, en el caso contrario, ignoraremos este parámetro.

En lo que respecta a la macro, esta ha sido creada para que resulte más cómodo añadir al archivo los parámetros del robot.

#define WRITE_BOT_PARAM(fileName,param) writer(fileName,"",#param+";"+(string)param);

En esta macro, nosotros utilizaremos las ventajas de las macros y formaremos una línea que contenga el nombre de la macro y su valor, mientras que solo transmitiremos a la salida la variable del parámetro de entrada. Mostraremos con mayor detalle su comodidad utilizando como ejemplo el uso de la macro. 

El método principal, SaveReportToFile, es bastante largo, por lo que solo mostraremos una parte del código. Lo único que hace es crear un ejemplar de la clase CDealHistoryGetter y obtener la matriz de la historia acumulada de transacciones, donde cada línea designa una transacción.

DealDetales history[];
CDealHistoryGetter dealGetter(_comission_manager);
dealGetter.getDealsDetales(history,0,TimeCurrent());

A continuación, tras comprobar si la historia está vacía, crea un ejemplar de la clase CReportCreator y obtiene las estructuras con los coeficientes principales:

if(ArraySize(history)==0)
   return;

CReportCreator reportCreator(_comission_manager);
reportCreator.Create(history,0);

TotalResult totalResult;
reportCreator.GetTotalResult(totalResult);
PL_detales pl_detales;
reportCreator.GetPL_detales(pl_detales);

Acto seguido, usando la función writer en el ciclo, guarda los datos de la historia. Una vez finalizado el ciclo, se añaden los campos con los siguientes coeficientes e indicadores:

writer(fileName,"","==========================================================================================================");
writer(fileName,"","PL;"+DoubleToString(totalResult.total.PL)+";");
int total_trades=pl_detales.total.profit.orders+pl_detales.total.drawdown.orders;
writer(fileName,"","Total trdes;"+IntegerToString(total_trades));
writer(fileName,"","Consecutive wins;"+IntegerToString(pl_detales.total.profit.dealsInARow));
writer(fileName,"","Consecutive DD;"+IntegerToString(pl_detales.total.drawdown.dealsInARow));
writer(fileName,"","Recovery factor;"+DoubleToString(totalResult.total.recoveryFactor)+";");
writer(fileName,"","Profit factor;"+DoubleToString(totalResult.total.profitFactor)+";");
double payoff=MathAbs(totalResult.total.averageProfit/totalResult.total.averageDD);
writer(fileName,"","Payoff;"+DoubleToString(payoff)+";");
writer(fileName,"","Drawdown by pl;"+DoubleToString(totalResult.total.maxDrawdown.byPL)+";");

Con esto, finaliza el funcionamiento del método. Ahora, vamos a mostrar la facilidad con la que podemos descargar la historia de transacciones, añadiendo esta posibilidad al robot del paquete estándar "Experts/Examples/Moving Average/Moving Average.mq5". En primer lugar, debemos incluir nuestro archivo:

#include <History manager/ShortReport.mqh>

A continuación, añadimos las variables a los parámetros de entrada que establecen la comisión de usuario y el deslizamiento:

input double custom_comission = 0; // Custom comission;
input int custom_shift = 0; // custom shift;

Si deseamos que la comisión y el deslizamiento que hemos indicado no se establezcan de forma condicional, sin como directiva (ver descripción de la clase CDealHistoryGetter en el artículo anterior), antes de incluir el archivo, determinaremos el parámetro ONLY_CUSTOM_COMISSION de la misma manera que en el ejemplo de abajo:

#define ONLY_CUSTOM_COMISSION
#include <History manager/ShortReport.mqh>

A continuación, crearemos la clase CCCM y, en el método OnInit, añadiremos la comisión y el deslizamiento a este ejemplar de la clase que guarda la comisión.

CCCM _comission_manager_;

...

int OnInit(void)
  {
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);  

...

  }

A continuación, añadimos en el método OnDeinit las siguientes líneas de código:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string file_name = __FILE__+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);

      WRITE_BOT_PARAM(file_name,MaximumRisk);      // Maximum Risk in percentage
      WRITE_BOT_PARAM(file_name,DecreaseFactor);   // Descrease factor
      WRITE_BOT_PARAM(file_name,MovingPeriod);     // Moving Average period
      WRITE_BOT_PARAM(file_name,MovingShift);      // Moving Average shift
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom comission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;
     }
  }

Ahora, tras eliminar un ejemplar del robot, siempre se comprobará la condición: ¿está iniciado en el simulador? Si está iniciado en el simulador, se llamará la función que guarda la historia de transacciones del robot en el archivo con el nombre "nombre_del_archivo_compilado Report.csv". Después de todos los datos que se escribirán en el archivo, añadimos otras 6 líneas: son los parámetros de entrada de este archivo. Y ahora, después de cada inicio de este experto en el simulador en modo de simulación, obtendremos un archivo con la descripción de la transacciones realizados por él, que se reescribirá cada vez que iniciemos un nuevo test. El archivo se guardará en el sandbox de archivos en el directorio Comon/Files.

Descargando la historia de operaciones dividida por transacciones

Ahora, vamos a analizar cómo descargar un informe de transacciones detallado, es decir, un informe de transacciones en el que cada operación no está agregada a una línea, sino dividida por posiciones, dentro de las cuales se encuentran todas las operaciones para dicha posición. Para esta tarea, necesitaremos el archivo DealsHistory.mqh, que ya contiene la inclusión del archivo ShortReport.mqh, por consiguiente, en nuestro robot de prueba se podrá incluir de inmediato solo el archivo DealsHistory.mqh y utilizar ambos métodos.

Este archivo contiene dos funciones. La primera de ellas es bastante común y se ha creado solo para sumar mejor las líneas:

void AddRow(string item, string &str)
  {
   str += (item + ";");
  }

La segunda escribe los datos en un archivo con la ayuda de la función writer, que ya hemos analizado. Vamos a mostrar su implementación al completo.

void WriteDetalesReport(string fileName,CCCM *_comission_manager)
  {

   if(FileIsExist(fileName,FILE_COMMON))
     {
      FileDelete(fileName,FILE_COMMON);
     }

   CDealHistoryGetter dealGetter(_comission_manager);

   DealKeeper deals[];
   dealGetter.getHistory(deals,0,TimeCurrent());

   int total= ArraySize(deals);

   string headder = "Asset;From;To;Deal DT (Unix seconds); Deal DT (Unix miliseconds);"+
                    "ENUM_DEAL_TYPE;ENUM_DEAL_ENTRY;ENUM_DEAL_REASON;Volume;Price;Comission;"+
                    "Profit;Symbol;Comment";

   for(int i=0; i<total; i++)
     {
      DealKeeper selected = deals[i];
      string asset = selected.symbol;
      datetime from = selected.DT_min;
      datetime to = selected.DT_max;

      for(int j=0; j<ArraySize(selected.deals); j++)
        {
         string row;
         AddRow(asset,row);
         AddRow((string)from,row);
         AddRow((string)to,row);

         AddRow((string)selected.deals[j].DT,row);
         AddRow((string)selected.deals[j].DT_msc,row);
         AddRow(EnumToString(selected.deals[j].type),row);
         AddRow(EnumToString(selected.deals[j].entry),row);
         AddRow(EnumToString(selected.deals[j].reason),row);
         AddRow((string)selected.deals[j].volume,row);
         AddRow((string)selected.deals[j].price,row);
         AddRow((string)selected.deals[j].comission,row);
         AddRow((string)selected.deals[j].profit,row);
         AddRow(selected.deals[j].symbol,row);
         AddRow(selected.deals[j].comment,row);

         writer(fileName,headder,row);

        }

      writer(fileName,headder,"");
     }


  }

Después de obtener los datos de las transacciones y crear el encabezado, procederemos a registrar el informe detallado de transacciones. Para ello, organizamos un ciclo con un ciclo incorporado: el ciclo principal itera las posiciones, mientras que el incorporado  itera las transacciones de estas posiciones. Después de registrar cada nueva posición (más exactamente, cada serie de transacciones que componen la posición), las posiciones se separarán las unas de las otras con un espacio en blanco: esto se ha pensado así para que resulte más cómodo leer el archivo en lo sucesivo. Como podemos adivinar, para añadir esta descarga al robot, no deberemos cambiar nada de manera cardinal, basta con llamarlo en el método OnDeinit, como el anterior:

void OnDeinit(const int reason)
  {
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string file_name = __FILE__+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);

      WRITE_BOT_PARAM(file_name,MaximumRisk);      // Maximum Risk in percentage
      WRITE_BOT_PARAM(file_name,DecreaseFactor);   // Descrease factor
      WRITE_BOT_PARAM(file_name,MovingPeriod);     // Moving Average period
      WRITE_BOT_PARAM(file_name,MovingShift);      // Moving Average shift
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom comission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;

      WriteDetalesReport(__FILE__+" Deals Report.csv", &_comission_manager_);
     }
  }

Para tener una imagen más clara de lo que sucede en la descarga, vamos a mostrar una plantilla del experto vacía añadiendo los métodos que garantizan la descarga del informe:

//+------------------------------------------------------------------+
//|                                                         Test.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define ONLY_CUSTOM_COMISSION
#include <History manager/DealsHistory.mqh>

input double custom_comission   = 0;       // Custom comission;
input int    custom_shift       = 0;       // custom shift;

CCCM _comission_manager_;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string arr[];
      StringSplit(__FILE__,'.',arr);
      string file_name = arr[0]+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom comission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;

      WriteDetalesReport(arr[0]+" Deals Report.csv", &_comission_manager_);
     }
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---

  }
//+------------------------------------------------------------------+

Ahora, añadiendo cualquier lógica a la plantilla vacía que hemos mostrado anteriormente, lograremos la formación de un informe una vez finalice el funcionamiento del experto en el simulador en el modo de prueba.

Envoltorio de DLL para crear la historia acumulada de transacciones

El primer artículo del presente ciclo se dedicó a la creación de una biblioteca DLL en el lenguaje C# que trabajase con informes de optimización. Recordemos: si para nuestros objetivos (la implementación de la optimización móvil continua), lo más cómodo es operar con archivos XML, ¿por qué hemos creado una biblioteca DLL que sabe leer, escribir y también clasificar los informes obtenidos? Del experto solo vamos a necesitar la funcionalidad de la escritura de datos, pero, dado que operar con funciones en estado puro es mucho más incómodo y laborioso que operar con objetos, hemos creado una clase de envoltorio para la funcionalidad de la descarga de datos. Este método se ubica en el archivo "XmlHistoryWriter.mqh", y se llama СXmlHistoryWriter. Aparte del objeto analizado, en él se define la estructura de los parámetros del robot requerida para transmitir la lista de parámetros del robot a este objeto. Como es habitual, vamos a analizar por orden todos los detalles de la implementación de esta funcionalidad. 

Para tener la posibilidad de crear un informe de optimización, incluiremos el archivo ReportCreator.mqh, y para implicar los métodos estáticos de la clase de la biblioteca DLL descrita en el primer artículo, importaremos dicha biblioteca, que deberá además ubicarse en el directorio MQL5/Librires.

#include "ReportCreator.mqh"
#import "ReportManager.dll"

Tras añadir los enlaces necesarios, deberemos ocuparnos de que resulte cómodo añadir los parámetros del robot a la colección de parámetros que se transmitirá posteriormente a la clase analizada. 

struct BotParams
  {
   string            name,value;
  };

#define ADD_TO_ARR(arr, value) \
{\
   int s = ArraySize(arr);\
   ArrayResize(arr,s+1,s+1);\
   arr[s] = value;\
}

#define APPEND_BOT_PARAM(Var,BotParamArr) \
{\
   BotParams param;\
   param.name = #Var;\
   param.value = (string)Var;\
   \
   ADD_TO_ARR(BotParamArr,param);\
}

Puesto que vamos a operar con una colección de objetos, será más sencillo trabajar con matrices dinámicas (en cualquier caso, resultará más familiar). Para que sea más cómodo añadir elementos a la matriz dinámica, hemos creado la macro ADD_TO_ARR, que cambia el tamaño de la colección y después añade a la misma el elemento transmitido. Hemos elegido precisamente una macro por su universalidad; por ello, ahora podemos añadir rápidamente valores de cualquier tipo a la matriz dinámica.

La siguiente macro opera propiamente con los parámetros. Esta macro crea un ejemplar de la estructura BotParams y lo añade a la matriz; además, en este caso, en la entrada solo se necesitará indicar la matriz a la que debemos añadir la descripción del parámetro y la variable que guarda este parámetro. La macro se encargará ella misma de asignar al parámetro un nombre igual al nombre de la variable, así como el valor de este parámetro convertido en formato de línea.

Necesitamos el formato de línea para que los ajustes en los archivos (*.set) y los datos guardados en el nuestro (*.xml) se correspondan correctamente. Como ya hemos visto en artículos anteriores, los archivos set guardan los parámetros de entrada de los robots en forma de clave-valor. En calidad de clave, se adoptará el nombre de la variable como en el código; en calidad de valor, se adoptará el valor asignado a este parámetro de entrada, además, todas las enumeraciones (num) se deberán establecer en el tipo int, y no como el resultado del funcionamiento de la función EnumToString(). La macro descrita precisamente convierte todos los parámetros en una línea del formato necesario. Todas las enumeraciones también se convierten primero en int, y después en formato de línea.

También se declara una función que permite copiar la matriz de parámetros del robot en otra matriz.

void CopyBotParams(BotParams &dest[], const BotParams &src[])
  {
   int total = ArraySize(src);
   for(int i=0; i<total; i++)
     {
      ADD_TO_ARR(dest,src[i]);
     }
  }

Necesitamos esta función porque la función estándar ArrayCopy se niega a trabajar con la matriz de estructuras. 

La propia clase de envoltorio se declara de la forma siguiente:

class CXmlHistoryWriter
  {
private:
   const string      _path_to_file,_mutex_name;
   CReportCreator    _report_manager;

   string            get_path_to_expert();//

   void              append_bot_params(const BotParams  &params[]);//
   void              append_main_coef(PL_detales &pl_detales,
                                      TotalResult &totalResult);//
   double            get_average_coef(CoefChartType type);
   void              insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);//
   void              append_days_pl();//

public:
                     CXmlHistoryWriter(string file_name,string mutex_name,
                     CCCM *_comission_manager);//
                     CXmlHistoryWriter(string mutex_name,CCCM *_comission_manager);
                    ~CXmlHistoryWriter(void) {_report_manager.Clear();} //

   void              Write(const BotParams &params[],datetime start_test,datetime end_test);//
  };

Para escribir en el archivo, en este se declaran dos campos de constante de línea:

El primero de ellos contiene la ruta al archivo en el que se escribirán los datos, mientras que el otro contiene el nombre de la exclusión mutua utilizada. La implementación de la exclusión mutua nombrada se ha sacado a la DLL C#, y es estándar. La propia exclusión mutua es necesaria porque el proceso de optimización tendrá lugar en diferenetes flujos en núcleos y procesos distintos (un inicio del robot - un proceso), por eso, podríamos encontrarnos con la situación indeseable en la que han finalizado dos optimizaciones, y dos o más procesos tratan de escribir simultáneamente los resultados en el mismo archivo, lo cual es inadmisible. Para eliminar este riesgo, se usa un objeto de sincronización basado en el núcleo del sistema operativo: la exclusión mutua nombrada. 

El ejemplar de la clase CReportCreator es imprescindible como campo, debido a que a este objeto recurrirán una serie de funciones, y no sería lógico crearlo de nuevo varias veces. Ahora, vamos a analizar la implementación de cada método.

Comenzaremos el análisis de la implementación por el constructor de la clase.

CXmlHistoryWriter::CXmlHistoryWriter(string file_name,
                                     string mutex_name,
                                     CCCM *_comission_manager) : _mutex_name(mutex_name),
   _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name),
   _report_manager(_comission_manager)
  {
  }
CXmlHistoryWriter::CXmlHistoryWriter(string mutex_name,
                                     CCCM *_comission_manager) : _mutex_name(mutex_name),
   _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+MQLInfoString(MQL_PROGRAM_NAME)+"_"+"Report.xml"),
   _report_manager(_comission_manager)
  {
  }

Esta clase contiene dos constructores que no resultan destacables por sí mismos. Sin embargo, debemos prestar atención al segundo constructor, que crea por sí mismo el nombre del archivo donde se guarda el informe de optimización. Lo que ocurre es que en el optimizador que analizaremos en el próximo artículo, existirá la posibilidad de establecer nuestros propios gestores de optimización, pero en el gestor de optimización que está implementado por defecto ya se ha implementado la correspondencia sobre el nombre de los archivos teniendo en cuenta los nombres generados por el robot, y precisamente es establecida por el segundo constructor. De acuerdo con esta correspondencia, el nombre del archivo comienza con el nombre del robot, seguido de un guión bajo y un postfijo "_Report.xml". Asimismo, a pesar de que la DLL puede escribir el archivo del informe en cualquier parte de la computadora, para acentuar al pertenencia de este archivo al trabajo del terminal, lo guardaremos siempre  en el directorio Comon del sandbox de MetaTrader5. 

Método que obtiene la ruta al experto:

string CXmlHistoryWriter::get_path_to_expert(void)
  {
   string arr[];
   StringSplit(MQLInfoString(MQL_PROGRAM_PATH),'\\',arr);
   string relative_dir=NULL;

   int total= ArraySize(arr);
   bool save= false;
   for(int i=0; i<total; i++)
     {
      if(save)
        {
         if(relative_dir== NULL)
            relative_dir=arr[i];
         else
            relative_dir+="\\"+arr[i];
        }

      if(StringCompare("Experts",arr[i])==0)
         save=true;
     }

   return relative_dir;
  }

La ruta al experto la vamos a necesitar para iniciar de forma automática dicho experto. Para ello, deberemos indicar su ruta en el archivo ini transmitido al iniciar el terminal, pero respecto al directorio Experts, no la ruta completa que obtenemos como resultado del funcionamiento de la función de obteneción de la ruta hasta el experto actual. Por eso, primero dividimos la ruta obtenida en componentes usando como separador la barra oblicua inversa, y después, comenzando por el primer directorio, buscamos en un ciclo el directorio "Experts". Cuando hayamos encontrado el directorio, formamos la ruta hasta el robot empezando por el siguiente directorio (o archivo del robot, si este se encuentra directamente en la carpeta raíz del directorio buscado).

Método append_bot_params:

Este método es un envoltorio para el método importado con un nombre análogo, y su implementación es la siguiente:

void CXmlHistoryWriter::append_bot_params(const BotParams &params[])
  {

   int total= ArraySize(params);
   for(int i=0; i<total; i++)
     {
      ReportWriter::AppendBotParam(params[i].name,params[i].value);
     }
  }

Transmitimos a la entrada de este método la matriz del robot anteriormente descrita. A continuación, en un ciclo para ca parámetro del robot, llamamos el método importado desde nuestra biblioteca.

El método append_main_coef tiene un implementación incluso más normal que el anterior, por eso, no tiene ningún sentido analizarla. Solo diremos que recibe en la entrada las estructuras obtenidas de la clase CReportCreator.

El método get_average_coef ha sido creado para calcular los valores medios de los coeficientes usando la media habitual de los coeficientes de los gráficos transmitidos. Se usa para calcular el beneficio medio y el factor de recuperación medio.  

El método insert_day es simplemente cómodo para llamar con un envoltorio al método importado ReportWriter::AppendDay, mientras que el método append_days_pl usa el ya mencionado envoltorio.

Entre estos métodos de envoltorio, existe un método público que actúa como principal, pues pone en marcha todo el mecanismo de guardado de datos: se trata del método Write.

void CXmlHistoryWriter::Write(const BotParams &params[],datetime start_test,datetime end_test)
  {
   if(!_report_manager.Create())
     {
      Print("##################################");
      Print("Can`t create report:");
      Print("###################################");
      return;
     }
   TotalResult totalResult;
   _report_manager.GetTotalResult(totalResult);
   PL_detales pl_detales;
   _report_manager.GetPL_detales(pl_detales);

   append_bot_params(params);
   append_main_coef(pl_detales,totalResult);

   ReportWriter::AppendVaR(totalResult.total.VaR_absolute.VAR_90,
                           totalResult.total.VaR_absolute.VAR_95,
                           totalResult.total.VaR_absolute.VAR_99,
                           totalResult.total.VaR_absolute.Mx,
                           totalResult.total.VaR_absolute.Std);

   ReportWriter::AppendMaxPLDD(pl_detales.total.profit.totalResult,
                               pl_detales.total.drawdown.totalResult,
                               pl_detales.total.profit.orders,
                               pl_detales.total.drawdown.orders,
                               pl_detales.total.profit.dealsInARow,
                               pl_detales.total.drawdown.dealsInARow);
   append_days_pl();

   string error_msg=ReportWriter::MutexWriter(_mutex_name,get_path_to_expert(),AccountInfoString(ACCOUNT_CURRENCY),
                    _report_manager.GetBalance(),
                    (int)AccountInfoInteger(ACCOUNT_LEVERAGE),
                    _path_to_file,
                    _Symbol,(int)Period(),
                    start_test,
                    end_test);
   if(StringCompare(error_msg,"")!=0)
     {
      Print("##################################");
      Print("Error while creating (*.xml) report file:");
      Print("_________________________________________");
      Print(error_msg);
      Print("###################################");
     }
  }

En caso de no lograr crear el informe con éxito, lo primero que hará es generar la entrada correspondiente en el log. Si el informe se ha creado con éxito, pasaremos a la obtención de los datos de los coeficientes buscados. A continuación, llamaremos por turno los métodos ya mencionados, que, como podemos leer en el primer artículo, añaden los parámetros solicitados a la clase que se encuentra en C#. Acto seguido, se llama al método que escribe los datos en el archivo. En caso de escritura incorrecta, error_msg contendrá el texto del error que se representará en los logs del simulador. 

La clase obtenida ya sabe formar el informe de transacciones por sí mismo, además de descargarlo (al llamar al método Write) en el archivo que ha sido condicionado al instanciar esta clase. No obstante, nos gustaría facilitarnos la vida aún más, y no tener que preocuparnos de otra cosa que los parámetros de entrada. Para ello precisamente, hemos creado la siguiente clase a analizar.

Nos referimos a la clase para la generación automática del informe de transacciones tras finalizar el proceso de simulación, CAutoUpLoader, que se encuentra en el archivo AutoLoader.mqh. Para trabajar con esta clase, deberemos añadir un enlace a la anterior clase descrita para la formación de informes en el formato XML.

#include <History manager/XmlHistoryWriter.mqh>

La propia signatura de esta clase es sencilla:

class CAutoUploader
  {
private:

   datetime          From,Till;
   CCCM              *comission_manager;
   BotParams         params[];
   string            mutexName;

public:
                     CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[]);
   virtual          ~CAutoUploader(void);

   virtual void      OnTick();

  };

Como podemos ver, la clase tiene el método sobrecargado OnTick, así como un destructor virtual. Todo esto es necesario para que pueda ser aplicada tanto con ayuda de la agregación, como con la ayuda de la herencia. Veamos de qué estamos hablando. La misión de esta clase consiste en reescribir en cada tick la hora de finalización del proceso de simulación, así como registrar la hora de comienzo de la simulación. Estos parámetros son necesarios para usar el objeto anteriormente analizado. Propiamente, tenemos varios enfoques para su aplicación: podemos, o bien instanciar simplemente este objeto en algún lugar en la clase del robot (si usted escribe robot con la ayuda de POO), o bien en el ámbito global, lo que le vendrá mejor a aquellos que escriben robots en el estilo C.

Después de ello, es necesario llamar al método OnTick() en la función OnTick() del ejemplar de esta clase, y ya tras eliminar el objeto de esta clase, se dará la descarga del informe de transacciones en su destructor. El segundo método de aplicación de la clase consiste simplemente en la herencia de este de la clase del robot; precisamente para ello se ha creado el destructor virtual y el método sobrecargado OnTick(). Como resultado de la aplicación del segundo método, no tendremos que monitorear en absoluto esta clase, solo trabajaremos con el robot. La implementación de esta clase es muy sencilla, como ya podíamos adivinar, lo único que hace es delegar su trabajo en la clase CXmlHistoryWriter:

void CAutoUploader::OnTick(void)
  {
   if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||
      MQLInfoInteger(MQL_TESTER)==1)
     {
      if(From == 0)
         From = iTime(_Symbol,PERIOD_M1,0);
      Till=iTime(_Symbol,PERIOD_M1,0);
     }
  }
CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager),
   mutexName(_mutexName)
  {
   CopyBotParams(params,_params);
  }
CAutoUploader::~CAutoUploader(void)
  {
   if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||
      MQLInfoInteger(MQL_TESTER)==1)
     {
      CXmlHistoryWriter historyWriter(mutexName,
                                      comission_manager);

      historyWriter.Write(params,From,Till);
     }
  }

Como la imagen general ya está bastante clara, vamos a ampliar la plantilla de descripción de robots mostrada anteriormente, añadiéndole la funcionalidad descrita:

//+------------------------------------------------------------------+
//|                                                         Test.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#define ONLY_CUSTOM_COMISSION
#include <History manager/DealsHistory.mqh>
#include <History manager/AutoLoader.mqh>

class CRobot;

input double custom_comission   = 0;       // Custom comission;
input int    custom_shift       = 0;       // custom shift;

CCCM _comission_manager_;
CRobot *bot;
const string my_mutex = "My Mutex Name for this expert";

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   _comission_manager_.add(_Symbol,custom_comission,custom_shift);

   BotParams params[];

   APPEND_BOT_PARAM(custom_comission,params);
   APPEND_BOT_PARAM(custom_shift,params);

   bot = new CRobot(&_comission_manager_,my_mutex,params);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(MQLInfoInteger(MQL_TESTER)==1)
     {
      string arr[];
      StringSplit(__FILE__,'.',arr);
      string file_name = arr[0]+" Report.csv";
      SaveReportToFile(file_name,&_comission_manager_);
      WRITE_BOT_PARAM(file_name,custom_comission); // Custom comission;
      WRITE_BOT_PARAM(file_name,custom_shift);     // custom shift;

      WriteDetalesReport(arr[0]+" Deals Report.csv", &_comission_manager_);
     }

   delete bot;
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   bot.OnTick();
  }
//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//| Clase principal del robot                                            |
//+------------------------------------------------------------------+
class CRobot : CAutoUploader
  {
public:
                     CRobot(CCCM *_comission_manager, string _mutexName, BotParams &_params[]) : CAutoUploader(_comission_manager,_mutexName,_params)
     {}

   void              OnTick() override;
  };

//+------------------------------------------------------------------+
//| Método para iniciar la lógica del robot                                  |
//+------------------------------------------------------------------+
void CRobot::OnTick(void)
  {
   CAutoUploader::OnTick();

   Print("Aquí tiene que encontrarse el inicio de la lógica del robot");
  }
//+------------------------------------------------------------------+

Bien, lo primero que tenemos que hacer es añadir el enlace al archivo donde se guarda nuestra clase de envoltorio para la descarga automática de informes en formato XML y predefinir la clase de nuestro robot, dado que resulta más sencillo implementarla y describirla al final del proyecto. En general, solemos hacer los algoritmos como proyectos MQL5: resulta mucho más cómodo que enfoque de una sola página, porque la clase con el robot y las clases acompañantes están divididas en archivos. No obstante, para que sea más fácil mostrar el ejemplo, lo hemos ubicado todo en un archivo.
A continuación, describimos nuestra clase; como ejemplo, usaremos una clase vacía con un único método sobrecargado OnTick. En este ejemplo, hemos recurrido al uso del modo de aplicación comercial de la clase CAutoUploader: la herencia. Debemos destacar que en el método sobrecargado OnTick tenemos que llamar explícitamente al método OnTick de la clase básica para que nuestro cálculo de fechas no se interrumpa, dado que de ello depende todo el funcionamiento de optimizador.

El siguiente paso será crear un puntero a la clase con el robot, puesto que es más cómodo rellenarlo desde el método OnInit que desde el ámbito global. Asimismo, crearemos una variable que guarde el nombre de la exclusión mutua.

En el método OnInit, instanciamos nuestro robot, y luego lo eliminamos en OnDeinit. Para que se transmita a nuestro robot la llamada de retorno sobre la llegada de un nuevo tick, llamaremos en el puntero al robot el método sobrecargado OnTick(), y en cuanto todas estas acciones sean ejecutadas, escribiremos nuestro robot en la clase CRobot.

Consideramos que las opciones de descarga de los informes de transacciones mediante la agregación o la creación de un ejemplar de la clase CAutoUpLoader en el ámbito global son semejantes entre sí, y deberían ser completamente comprensibles. Si el lector tiene alguna duda, no dude en plantearla. 

De esta forma, usando esta plantilla de escritura de robots, o añadiendo las llamadas correspondientes a sus propios algoritmos ya existentes, usted podrá usarlos de forma conjunta con el optimizador del que hablaremos en el próximo artículo.

Conclusión

Una vez analizado en el primer artículo el mecanismo del trabajo con los archivos XVL de un informe, hemos pasado al proceso de formación de los propios informes, al que está dedicado este artículo. En el presente artículo, hemos analizado el mecanismo de formación de los informes, comenzando por los objetos que descargan la historia de transacciones, y terminando con los objetos que crean el propio informe. Durante el análisis de los objetos que participan en la creación del informe, hemos analizado con especial cuidado la parte computacional, mostrando las fórmulas de los coeficientes principales; asimismo, hemos descrito con el mayor detalle los posibles problemas que podrían surgir en los cálculos.

Como ya hemos dicho en la introducción a este artículo, los objetos aquí descritos, constituyendo un puente entre el mecanismo de descarga de datos y el mecanismo encargado de su creación, no son menos importantes que estos, por lo que merecen una descripción detallada. Aparte de las funciones que guardan los archivos con los informes de las transacciones, también hemos descrito las clases que participan en la descarga de los informes XML, además de describir las plantillas de los robots que utilizarán esta funcionalidad automáticamente. Asimismo, hemos descrito el mecanismo de adición de la funcionalidad creada a cualquier algoritmo ya existente, lo cual permitirá a los usuarios del optimizador automático optimizar sus desarrollos tantos antiguos, como nuevos.   

En el fichero adjunto se encuentran dos carpetas, ambas deberán ser descomprimidas en el directorio MQL/Include. Además, tendremos que añadir al directorio MQL5/Libraries la biblioteca  ReportManager.dll, que deberemos tomar del artículo anterior.

El fichero contiene los siguientes archivos:

  1. CustomGeneric
    • GenericSorter.mqh
    • ICustomComparer.mqh
  2. History manager
    • AutoLoader.mqh
    • CustomComissionManager.mqh
    • DealHistoryGetter.mqh
    • DealsHistory.mqh
    • ReportCreator.mqh
    • ShortReport.mqh
    • XmlHistoryWriter