English Русский 中文 Deutsch 日本語 Português
Las 100 mejores pasadas de optimización (Parte 1). Creando un analizador de optimizaciones

Las 100 mejores pasadas de optimización (Parte 1). Creando un analizador de optimizaciones

MetaTrader 5Probador | 28 enero 2019, 07:43
2 842 0
Andrey Azatskiy
Andrey Azatskiy

Introducción

Las tecnologías modernas ya se han afianzado con bastante seguridad en la esfera comercial de los mercados financieros, y ahora es prácticamente imposible imaginar cómo podríamos trabajar sin ellas en este ámbito. Y es que, hasta hace relativamente poco, el comercio se realizaba de forma manual, existía un lenguaje gestual completo (ahora está despareciendo a toda velocidad) que describía quá parte del activo se necesitaba comprar o vender.

El mundo computacional ha apartado bastante deprisa este método de comercio, trayendo al mismo tiempo el trading por internet a la casa de todo aquel que lo desee. Ahora podemos mirar las cotizaciones de los activos en tiempo real y tomar las decisiones correspondientes. Y lo que es más: con la llegada de las tecnologías de internet, el comercio manual ha comenzado a desaparecer de la industria bursátil en esta esfera. En la actualidad, más de la mitad de las transacciones son ejecutadas por algoritmos comerciales, y no estará de más decir que entre los terminales más cómodos en este ámbito, MetaTrader 5 ocupa el primer puesto.

Pero, a pesar de las numerosas ventajas de esta plataforma, también tiene una serie de defectos que hemos intentado compensar escribiendo ciertas aplicaciones. Este artículo describe el proceso creativo de un programa escrito totalmente en el lenguaje MQL5 usando la biblioteca EasyAndFastGUI, que está llamada a mejorar el proceso de selección de los parámetros de optimización de los algoritmos comerciales. Asimismo, añade posibilidades al análisis del comercio en retrospectiva y la valoración del trabajo en general.



En primer lugar, la optimización de asesores ocupa bastante tiempo. Claro que esto se debe a que el simulador genera los ticks con mayor calidad (incluso eligiendo OHLC, se generan 4 ticks en cada vela) y otros elementos complementarios que permiten evaluar al asesor con mayor efectividad. Sin embargo, en las computadoras domésticas, que no son tan potentes como desearíamos, el proceso de optimización puede prolongarse por días o semanas. Con frecuencia, sucede que, tras elegir los parámetros del robot, comprendemos casi enseguida que no eran los correctos, y aparte de la descarga con las estadísticas de las pasadas de optimización y varios coeficientes de evaluación, no tenemos nada a mano.

Querríamos tener estadísticas completas de cada pasada de optimización y la posibilidad de filtrar (incluidos los filtros condicionales) para cada una de ellas según un gran número de parámetros. Así que no estaría mal comparar las estadísticas de las transacciones con la estrategia "Buy And Hold" y superponer las estadísticas unas a otras. Además, a veces se necesita descargar todos los datos de la historia comercial a un archivo para el posterior procesamiento de los resultados de las transacciones.

En ocasiones, es necesario ver qué deslizamiento podrá soportar el algoritmo y cómo se comportará en un determinado intervalo temporal. Y es que algunas estrategias dependen del tipo de mercado. Un ejemplo de ello puede ser una estrategia centrada en el flat. Cuando comienzan los intervalos de tendencia, empezará a perder dinero con toda seguridad, ganando durante los movimientos laterales. Tampoco estaría mal ver por separado del gráfico de PL intervalos aparte (según la fecha). Y no simplemente gráficos de precio, sino conjuntos completos de coeficientes y otros complementos.

Asimismo, merece la pena prestar atención a las simulaciones en tiempo real, que son muy informativas, pero cuyos gráficos en el informe estándar del simulador de estrategias se representan como una continuación del anterior. Los no iniciados podrán considerar a la ligera que el robot ha perdido bruscamente todo lo ganado, y que después ha comenzado a recuperarse (o lo que es peor, a entrar en negativo). En el programa presentado, todos los datos se ofrecen según el tipo de optimización (o bien en tiempo real, o bien histórica).

Asimismo, otro detalle bastante importante será recordar el «Santo Grial», que tanto buscan muchos de los constructores de estrategias comerciales. Algunos robots proporcionan un 1000% y más al mes. Podría parecer que superan al mercado (nos referimos a la estrategia "Buy and Hold"), sin embargo, en la práctica todo es un poco distinto. Como muestra el programa presentado, estos robots de verdad pueden conseguir un 1000%, y sin embargo, no superan al mercado.

En el programa se separa el análisis entre transacciones con un robot con un lote completo (incrementándolo/reduciéndolo, etc.), y la imitación de una transacción por parte del robot con un solo lote (lote mínimo disponible para comerciar). Cuando se construye el gráfico del comercio de "Buy And Hold", el programa presentado tiene en cuenta la gestión del lote que ha hecho el robot (es decir, compra más activo cuando ha aumentado el lote y reduce la cantidad del activo comprado cuando el lote ha disminuido). Si comparamos los dos gráficos, entenderemos que el robot de prueba, que en una de las mejores pasadas de optimización ha mostrado unos resultados excepcionales, aun así no ha podido superar al mercado. Por eso, para evaluar con sangre fría las estrategias comerciales, merece la pena echar un vistazo al gráfico con un lote, donde, tanto el robot de PL, como la estrategia de PL "Buy and Hold" se muestran como si comerciaran con el volumen mínimo permitido (PL= Profit/Loss — gráfico de beneficio obtenido por tiempo).

A continuación, vamos a analizar con mayor detalle cómo se ha creado este programa.


Estructura del analizador de optimizaciones

Dese el punto de vista gráfico, la estructura del programa presentado se puede expresar de la forma siguiente:


El analizador de optimizaciones resultante no está vinculado a ningún robot en concreto, y no es parte del mismo. Sin embargo, debido al carácter específico de la construcción de las interfaces gráficas en MQL5, como base principal ha actuado la plantilla MQL5 para la creación de asesores. Puesto que el programa obtenido ha sido bastante grande (varios miles de líneas de código), para aumentar la concreción y la consistencia, se ha dividido en una serie de bloques (representados más arriba en el diagrama), que se dividen en las clases de las que se componen. La plantilla del robot es solo un punto de partida para comenzar la aplicación. Cada uno de los bloques se analizará abajo con mayor detalle. Ahora mismo vamos a describir relación mutua entre ellos. Para trabajar con la aplicación, necesitaremos:

  • Un algoritmo comercial
  • Dll Sqlite3
  • La biblioteca de interfaz gráfica anteriormente mencionada, con las correcciones necesarias (descritas en el bloque de trabajo con el gráfico)

El propio robot puede haber sido escrito como sea (con ayuda de POO, simplemente una función dentro de la plantilla de robot, importación desde Dll…), lo importante es que use la plantilla para la escritura de robots ofrecida por el Wizard MQL5. A esta se conecta un archivo del bloque de trabajo con la base de datos donde se encuentra la clase que descarga los datos necesarios en la base de datos al suceder cada pasada de optimización. Esta es una parte independiente, que no depende en forma alguna del funcionamiento de la propia aplicación, puesto que la base de datos se forma durante el inicio del robot en el simulador de estrategias.

El bloque que se ocupa de los cálculos supone una continuación perfeccionada del artículo «Representación personalizada de la historia comercial y creación de gráficos para los informes».

Los bloques con la base de datos y el bloque de cálculos se usan tanto en el robot analizado, como en el programa descrito. Por eso los hemos sacado al directorio «Include». Estos bloques ejecutan la parte principal del trabajo y están conectados con la interfaz gráfica a través de la clase presenter.

La clase presenter conecta los bloques separados del programa - cada uno de los cuales cumple su papel - y la interfaz gráfica. En ella se procesan los eventos de pulsación de botón y otros, y tiene lugar el redireccionamiento a otros bloques lógicos. Los datos obtenidos de ellos retornan al presenter, donde se procesan y se construyen los gráficos de acuerdo con ellos, se rellenan los recuadros y tienen lugar otras interacciones con la parte gráfica.

La parte gráfica del programa no ejecuta ninguna lógica conceptual. Lo único que hace es construir una ventana con la interfaz necesaria y, durante el evento de pulsación al botón, llamar mediante el usuario las funciones correspondientes del presenter.

El propio programa ha sido escrito en forma de proyecto MQL5, esto permite enfocar su escritura de una manera más estructurada y componer en un único lugar todos los archivos necesarios con el código. En el proyecto se ha incluido otra clase más, que se analizará en el bloque que describe los cálculos. Esta clase se ha escrito especialmente para este programa, y su tarea consiste en filtrar las pasadas de optimización según el método que hemos desarrollado. En esencia, sirve a toda la pestaña «Optimisation selection». El resultado de su funcionamiento es la reducción de la muestra de datos según determinados criterios.

La clase de clasificación universal es una adición independiente al programa, que no encaja para el análisis en ninguno de los bloques, pero que al mismo tiempo representa una parte suya bastante importante. Por eso, la veremos brevemente en esta parte del artículo.

Como podemos entender por el nombre, la clase analizada se ocupa de la clasificación de los datos. Hemos tomado prestado su algoritmo de un sitio web de terceros Clasificación mediante selección (en ruso).

//+------------------------------------------------------------------+
//| E-num con estilo de clasificación                                |
//+------------------------------------------------------------------+
enum SortMethod
  {
   Sort_Ascending,// Ascendente
   Sort_Descendingly// Descendente
  };
//+------------------------------------------------------------------+
//| Clase que clasifica el tipo de datos transmitido                 |
//+------------------------------------------------------------------+
class CGenericSorter
  {
public:
   // Constructor por defecto
                     CGenericSorter(){method=Sort_Descendingly;}
   // Método de clasificación
   template<typename T>
   void              Sort(T &out[],ICustomComparer<T>*comparer);
   // Eligiendo el método de clasificación
   void Method(SortMethod _method){method=_method;}
   // Obteniendo el método de clasificación
   SortMethod Method(){return method;}
private:
   // Método de clasificación
   SortMethod        method;
  };

La clase contiene el método de plantilla Sort, que precisamente organiza los datos. Gracias al método de plantilla, puede ordenar cualquier dato transmitido, incluidas las clases y estructuras. La metodología de comparación de datos deberá describirse en una clase aparte, que implemente la interfaz IСustomComparer<T>. Hemos necesitado crear nuestra propia interfaz del tipo IСomparer solo porque en la interfaz tradicional IСomparer, en el método Compare, los datos auxiliares no se transmiten por enlace, sino que la transmisión por enlace es preciasamente una de las condiciones de transmisión de las estructuras al método en el lenguaje MQL5.

Las sobrecargas del método de clase CGenericSorter::Method retornan y reciben el tipo de clasificación de datos (en orden creciente o decreciente). En todos los bloques de este programa donde se ordenan los datos, se usa precisamente esta clase.


Gráficos

¡Advertencia!


Al crear la interfaz gráfica, se detectó un bug en la biblioteca utilizada (EasyAndFastGUI), que seguía actual a la fecha de escritura del artículo. Debido a este bug, el elemento gráfico ComboBox no limpiaba por completo algunas variables durante el nuevo rellenado. De acuerdo con las recomendaciones del creador de la biblioteca, para repararlo debemos introducir las siguientes correcciones:

m_item_index_focus =WRONG_VALUE;
m_prev_selected_item =WRONG_VALUE;
m_prev_item_index_focus =WRONG_VALUE;

в метод CListView::Clear(const bool redraw=false).

Este método se encuentra en la línea 600 en el archivo ListView.mqh. En el archivo ubicado en la ruta:
Include\EasyAndFastGUI\Controls.

Si usted no añade esta corrección, en ocasiones surgirá el error "Array out of range" al abrir ComboBox, y la aplicación se cerrará forzosamente.


Para crear una ventana en MQL5 basada en la biblioteca EasyAndFastGUI, es necesario crear una clase que actúe como contenedor para el posterior rellenado de la ventana, y heredar de la clase CwindEvents. Dentro de la clase, será necesario redefinir los métodos:

 //--- Inicialización/desinicialización
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- Manejador de eventos del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);//

En general, la plantilla para crear la ventana deberá ser la siguiente:

class CWindowManager : public CWndEvents
  {
public:
                     CWindowManager(void){presenter = NULL;};
                    ~CWindowManager(void){};
   //===============================================================================   
   // Calling methods and events :
   //===============================================================================
   //--- Inicialización/desinicialización
   void              OnDeinitEvent(const int reason){CWndEvents::Destroy();};
   //--- Manejador de eventos del gráfico
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

//--- Creando la interfaz gráfica del programa
   bool              CreateGUI(void);


private:
 //--- Ventana principal
   CWindow           m_window;
  }

La ventana en sí se crea indicando el tipo Cwindow dentro de la clase, pero antes de representarla, será necesario definir una serie de propiedades de la ventana. En este caso concreto, el método de creación de la ventana tendrá el aspecto que sigue:

bool CWindowManager::CreateWindow(const string text)
  {
//--- Añadimos el puntero de la ventana a la matriz de ventanas
   CWndContainer::AddWindow(m_window);
//--- Coordenadas
   int x=(m_window.X()>0) ? m_window.X() : 1;
   int y=(m_window.Y()>0) ? m_window.Y() : 1;
//--- Propiedades
   m_window.XSize(WINDOW_X_SIZE+25);
   m_window.YSize(WINDOW_Y_SIZE);
   m_window.Alpha(200);
   m_window.IconXGap(3);
   m_window.IconYGap(2);
   m_window.IsMovable(true);
   m_window.ResizeMode(false);
   m_window.CloseButtonIsUsed(true);
   m_window.FullscreenButtonIsUsed(false);
   m_window.CollapseButtonIsUsed(true);
   m_window.TooltipsButtonIsUsed(false);
   m_window.RollUpSubwindowMode(true,true);
   m_window.TransparentOnlyCaption(true);

//--- Establecemos las pistas emergentes
   m_window.GetCloseButtonPointer().Tooltip("Close");
   m_window.GetFullscreenButtonPointer().Tooltip("Fullscreen/Minimize");
   m_window.GetCollapseButtonPointer().Tooltip("Collapse/Expand");
   m_window.GetTooltipButtonPointer().Tooltip("Tooltips");
//--- Creando el formulario
   if(!m_window.CreateWindow(m_chart_id,m_subwin,text,x,y))
      return(false);
//---
   return(true);
  }

Una condición obligatoria de este método es la línea de adición de la ventana a la matriz de ventanas de la aplicación, así como la creación de formulario. En lo sucesivo, durante el funcionamiento de la aplicación, al activarse el evento OnEvent, uno de los métodos de creación de la biblioteca gráfica de interfaces iterará en el ciclo por todas las ventanas introducidas en la matriz de ventanas. A continuación, itera por todos los elementos dentro de esta ventana y busca un evento relacionado con la pulsación en alguna interfaz de gestión, o bien la selección de una línea en el recuadro, etc... Por eso, al crear cada nueva ventana de la aplicación será necesario añadir el enlace a esta ventana a la matriz de enlaces.

La aplicación creada tiene una interfaz dividida en pestañas. En total se han creado 4 contenedores de pestañas:

//--- Tabs
   CTabs             main_tab; // Pestañas principales
   CTabs             tab_up_1; // Pestañas con ajustes y recuadro de resultados
   CTabs             tab_up_2; // Pestañas con estadísticas y selección de parámetros, y también con los gráficos comunes
   CTabs             tab_down; // Pestañas con estadísticas y descarga a un archivo

En el formulario, estas tienen el siguiente aspecto (descrito en rojo en la captura de pantalla):

  • main_tab — separa el recuadro con todas las pasadas de optimización seleccionadas ("Optimisation Data") del resto de la interfaz del programa. En este recuadro se rellenan todos los resultados que satisfagan las condiciones del filtro en la pestaña con los ajustes. Después, los resultados obtenidos se ordenan según el coeficiente elegido en "ComboBox — Sort by". Ya en su aspecto clasificado, los datos obtenidos se trasladan al recuadro descrito. La pestaña con el resto de la interfaz del programa contiene otros 3 contenedores Tab.
  • tab_up_1 — contiene una división en los ajustes primarios del programa y un recuadro con los resultados filtrados. Pestaña "Settings" - aparte de las condiciones de los filtros descritas, sirve para elegir la base de datos e introducir información adicional. Por ejemplo, podemos elegir si merece o no la pena introducir en el recuadro con los resultados de la selección de información aquellos datos que han sido introducidos en el recuadro en la pestaña "Optimisation Data", o una cantidad determinada de mejores parámetros (filtrado descendente según el coeficiente elegido).
  • tab_up_2 — contiene 3 pestañas, cada una de las cuales contiene una interfaz que ejecuta 3 tareas diferentes. La primera pestaña contiene un informe concreto de la pasada de optimización seleccionada, y permite modelar el deslizamiento, así como analizar la historia comercial de un intervalo temporal determinado. La segunda sirve como filtro para las pasadas de optimización, y ayuda, en primer lugar, a definir la sensibilidad de la estrategia con respecto a diversos parámetros, y en segundo lugar, a reducir el número de resultados de optimización, eligiendo los intervalos más adecuados de los parámetros que le interesen. La última pestaña sirve como representación gráfica del recuadro de resultados de optimización y muestra el número total de parámetros de optimización seleccionados.
  • tab_down — cuenta con 5 pestañas, 4 de las cuales representan el informe comercial de este robot en el proceso de optimización con los parámetros elegidos. La última pestaña representa la descarga de los datos a un archivo. La primera pestaña presenta un recuadro con coeficientes de valoración. La segunda pestaña, la distribución de beneficio/pérdidas según los días de comercio. La tercera es una presentación del gráfico de beneficio y pérdidas, superpuesto a la estrategia "Buy and Hold" (gráfico negro). La cuarta pestaña representa el cambio de ciertos coeficientes elegidos en el tiempo, así como varios tipos de gráficos adicionales, interesantes e informativos, que se pueden obtener analizando los resultados de las transacciones del robot.

El proceso de creación de las pestañas es semejante, solo se diferencia en el relleno. Vamos a mostrar como ejemplo el método que crea la pestaña principal:

//+------------------------------------------------------------------+
//| Main Tab                                                         |
//+------------------------------------------------------------------+
bool CWindowManager::CreateTab_main(const int x_gap,const int y_gap)
  {
//--- Guardamos el puntero al elemento principal
   main_tab.MainPointer(m_window);

//--- Matriz con la anchura para las pestañas
   int tabs_width[TAB_MAIN_TOTAL];
   ::ArrayInitialize(tabs_width,45);
   tabs_width[0]=120;
   tabs_width[1]=120;
//---
   string tabs_names[TAB_UP_1_TOTAL]={"Analysis","Optimisation Data"};
//--- Propiedades
   main_tab.XSize(WINDOW_X_SIZE-23);
   main_tab.YSize(WINDOW_Y_SIZE);
   main_tab.TabsYSize(TABS_Y_SIZE);
   main_tab.IsCenterText(true);
   main_tab.PositionMode(TABS_LEFT);
   main_tab.AutoXResizeMode(true);
   main_tab.AutoYResizeMode(true);
   main_tab.AutoXResizeRightOffset(3);
   main_tab.AutoYResizeBottomOffset(3);
//---
   main_tab.SelectedTab((main_tab.SelectedTab()==WRONG_VALUE)? 0 : main_tab.SelectedTab());
//--- Añadimos las pestañas con las propiedades indicadas
   for(int i=0; i<TAB_MAIN_TOTAL; i++)
      main_tab.AddTab((tabs_names[i]!="")? tabs_names[i]: "Tab "+string(i+1),tabs_width[i]);
//--- Creamos el elemento de control
   if(!main_tab.CreateTabs(x_gap,y_gap))
      return(false);
//--- Añadimos el objeto a la matriz general de grupos de objetos
   CWndContainer::AddToElementsArray(0,main_tab);
   return(true);
  }

Aparte del relleno, que puede variar, las principales líneas de código son:

  1. Adición del puntero al elemento principal: es necesario que el contenedor de pestañas sepa a qué elemento está fijado
  2. Línea de creación del elemento de control
  3. Adición del elemento a la lista general de elementos de control.

Los siguientes en cuanto a jerarquía serán los elementos de control. En esta aplicación se han usado 11 tipos de elementos de control. Todos ellos se crean de manera semejante, y por eso, para crear cada uno de ellos, se han escrito métodos que añaden estos elementos de control. Analicemos la implementación de solo uno de ellos:

bool CWindowManager::CreateLable(const string text,
                                 const int x_gap,
                                 const int y_gap,
                                 CTabs &tab_link,
                                 CTextLabel &lable_link,
                                 int tabIndex,
                                 int lable_x_size)
  {
//--- Guardamos el puntero al elemento principal
   lable_link.MainPointer(tab_link);
//--- Fijar a la pestaña
   tab_link.AddToElementsArray(tabIndex,lable_link);

//--- Ajustes
   lable_link.XSize(lable_x_size);

//--- Creando
   if(!lable_link.CreateTextLabel(text,x_gap,y_gap))
      return false;

//--- Añadimos el objeto a la matriz general de grupos de objetos
   CWndContainer::AddToElementsArray(0,lable_link);
   return true;
  }

El elemento de control transmitido es CTextLabel, al igual que en las pestañas, debe recordar el elemento al que está asignado como contenedor. El contenedor de pestañas, a su vez, recuerda en qué pestaña precisamente se encuentra el elemento. Después de ello, viene el relleno del elemento con los ajustes necesarios y los datos originales. Al final, tiene lugar la adición del objeto a la matriz general de objetos.

Por analogía con los rótulos, se añaden también otros elementos definidos dentro de la clase contenedor como campos. Hemos separado los elementos determinados y hemos ubicado parte de ellos en la zona protected de la clase: son aquellos elementos para los que no será necesario acceso desde el presenter. La otra parte la hemos ubicado en public: son aquellos elementos que determinan algunas condiciones, o bien botones de radio, cuyo estado de pulsación merece la pena comprobar desde el presenter. En otras palabras, los encabezados de todos los elementos y métodos cuyo acceso no es deseable se encuentran en la parte protected y private de la clase, donde también se encuentra el enlace al presenter. La propia adición del enlace al presenter se ha ejecutado en forma de método público, en cuyo comienzo se comprueba la presencia del presenter ya añadido. Solo después, si el enlace a este no ha sido añadido, se guarda el presenter. Esto se ha hecho para evitar la posibilidad de sustituir dinámicamente el presenter durante la ejecución del programa.

La creación en sí de la propia ventana tiene lugar en el método CreateGUI:

bool CWindowManager::CreateGUI(void)
  {
//--- Create window
   if(!CreateWindow("Optimisation Selection"))
      return(false);

//--- Create tabs
   if(!CreateTab_main(120,20))
      return false;
   if(!CreateTab_up_1(3,44))
      return(false);
   int indent=WINDOW_Y_SIZE-(TAB_UP_1_BOTTOM_OFFSET+TABS_Y_SIZE-TABS_Y_SIZE);
   if(!CreateTab_up_2(3,indent))
      return(false);
   if(!CreateTab_down(3,33))
      return false;

//--- Create controls 
   if(!Create_all_lables())
      return false;
   if(!Create_all_buttons())
      return false;
   if(!Create_all_comboBoxies())
      return false;
   if(!Create_all_dropCalendars())
      return false;
   if(!Create_all_textEdits())
      return false;
   if(!Create_all_textBoxies())
      return false;
   if(!Create_all_tables())
      return false;
   if(!Create_all_radioButtons())
      return false;
   if(!Create_all_SepLines())
      return false;
   if(!Create_all_Charts())
      return false;
   if(!Create_all_CheckBoxies())
      return false;

// Show window
   CWndEvents::CompletedGUI();

   return(true);
  }

Como podemos ver por su implementación, él propiamente no crea un solo elemento de control, sino que llama otros métodos de creación de estos elementos. La principal línea de código que deberá ir inmediatamente como línea final en este método es CWndEvents::CompletedGUI();

Esta línea finaliza el gráfico y lo dibuja en la pantalla del usuario. La creación de cada elemento de control concreto - ya sean las líneas separadoras de los rótulos, o botones - se muestra en los métodos de rellenado originales, que usan los métodos de creación de elementos de control gráficos analizados más arriba. Los encabezados de estos métodos se encuentran en la parte private de la clase:

//===============================================================================   
// Controls creation:
//===============================================================================
//--- All Lables
   bool              Create_all_lables();
   bool              Create_all_buttons();
   bool              Create_all_comboBoxies();
   bool              Create_all_dropCalendars();
   bool              Create_all_textEdits();
   bool              Create_all_textBoxies();
   bool              Create_all_tables();
   bool              Create_all_radioButtons();
   bool              Create_all_SepLines();
   bool              Create_all_Charts();
   bool              Create_all_CheckBoxies();

Al hablar sobre la creación de un gráfico, es imposible no prestar atención a la parte responsable del modelo de eventos. Para realizar correctamente el procesamiento en las aplicaciones gráficas escritas con la ayuda de EasyAndFastGUI, necesitaremos ejecutar las siguientes acciones:

Crear el método del manejador de eventos (por ejemplo, la pulsación del botón). Este método debe adoptar como parámetros id y lparam. El primer parámetro indica el tipo de evento gráfico, y el segundo, la ID del objeto con el que ha habido interacción. La implementación de estos métodos es semejante en todos los casos:

//+------------------------------------------------------------------+
//| Btn_Update_Click                                                 |
//+------------------------------------------------------------------+
void CWindowManager::Btn_Update_Click(const int id,const long &lparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON && lparam==Btn_update.Id())
     {
      presenter.Btn_Update_Click();
     }
  }

Para comenzar, necesitaremos comprobar la condición (si se ha pulsado el botón, o bien se ha elegido el elemento…). La segunda estapa es la comprobación según lparam, donde se compara la ID transmitida al método con la ID del elemento necesario de la lista.

Todas las declaraciones de eventos de procesamiento de pulsación del botón se encuentran en la parte private de la clase. Pero, para que haya reacción al evento, también tenemos que llamarlo. La llamada de los eventos declarados se realiza en el método sobrecargado OnEvent:

//+------------------------------------------------------------------+
//| OnEvent                                                          |
//+------------------------------------------------------------------+
void CWindowManager::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   Btn_Update_Click(id,lparam);
   Btn_Load_Click(id,lparam);
   OptimisationData_inMainTable_selected(id,lparam);
   OptimisationData_inResults_selected(id,lparam);
   Update_PLByDays(id,lparam);
   RealPL_pressed(id,lparam);
   OneLotPL_pressed(id,lparam);
   CoverPL_pressed(id,lparam);
   RealPL_pressed_2(id,lparam);
   OneLotPL_pressed_2(id,lparam);
   RealPL_pressed_4(id,lparam);
   OneLotPL_pressed_4(id,lparam);
   SelectHistogrameType(id,lparam);
   SaveToFile_Click(id,lparam);
   Deals_passed(id,lparam);
   BuyAndHold_passed(id,lparam);
   Optimisation_passed(id,lparam);
   OptimisationParam_selected(id,lparam);
   isCover_clicked(id,lparam);
   ChartFlag(id,lparam);
   show_FriquencyChart(id,lparam);
   FriquencyChart_click(id,lparam);
   Filtre_click(id,lparam);
   Reset_click(id,lparam);
   RealPL_pressed_3(id,lparam);
   OneLotPL_pressed_3(id,lparam);
   ShowAll_Click(id,lparam);
   DaySelect(id,lparam);
  }

Que a su vez se llama desde la plantilla del robot. De esta forma, el modelo de eventos se extiende desde la plantilla del robot (mostrado más abajo) hacia la interfaz gráfica. En la GUI se procesan y filtran los eventos que nos interesan, y a continuación son redirigidos para el posterior procesamiento en el presenter. La propia plantilla supone un punto de partida para el comienzo del programa, y tiene el aspecto siguiente:

#include "Presenter.mqh"

CWindowManager _window;
CPresenter Presenter(&_window);
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!_window.CreateGUI())
     {
      Print(__FUNCTION__," > ¡No se ha logrado crear la interfaz gráfica!");
      return(INIT_FAILED);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   _window.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   _window.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Trabajando con la base de datos

Antes de analizar esta parte del proyecto, bastante amplia, merece la pena decir unas cuantas palabras sobre la elección realizada. Inicialmente, se estableció como uno de los objetivos del proyecto la tarea siguiente: la posibilidad de trabajar con los resultados de optimización después de finalizar la propia optimización, así como lograr la accesibilidad de estos resultados en cualquier momento. El guardado de datos en un archivo se descartó de inmediato, debido a su falta de funcionalidad. Para que el trabajo con archivos fuera plenamente funcional, necesitábamos crear varios recuadros, que en realidad serían un gran recuadro, pero con diferente número de líneas, o bien varios archivos.

Tanto lo uno como lo otro es incómodo, y bastante más complejo en su implementación. El segundo método que viene a la cabeza es la creación de frames de optimización. Se trata de una herramienta estupenda, pero, en primer lugar, no era el objetivo del trabajo con la optimización durante la propia optimización, y, en segundo lugar, la funcionalidad de los frames no es tan buena como la de las bases de datos. Además, los frames han sido diseñados pensando en MetaTrader, mientras que las bases de datos se pueden usar en cualquier programa analítico de terceros, de ser necesario.

Elegir una base de datos conveniente resultó bastante sencillo. Necesitábamos una base de datos de difusión rápida, fácil de conectar y que no exigiera ningún software adicional. La base de datos Sqlite cumple con todos estos requisitos, y precisamente por estas características es tan popular. Para usarla se necesita conectar al proyecto las Dll ofrecidas por el proveedor de la base. Estas dll se han escrito en el lenguaje C y conectan fácilamente con los programas MQL5, lo cual es un plus muy agradable, porque no necesitamos escribir ni una línea de código en un lenguaje ajeno, complicando con ello el proyecto. Entre las desventajas de este enfoque, encontramos que en la Dll Sqlite no se ofrece una API cómoda para trabajar con la base de datos, y por eso necesitaremos escribir aunque sea un envoltorio mínimo para trabajar con la base. En el artículo «SQL Y MQL5: TRABAJANDO CON LA BASE DE DATOS SQLITE» se muestra bastante bien un ejemplo de escritura de una funcionalidad semejante. Para este proyecto, se ha utilizado parte del código del artículo indicado, pero solo aquella parte relacionada con la interacción con WinApi y la importación de ciertas funciones de dll a MQL5. En lo que respecta al envoltorio, hemos tomado la decisión de escribirlo por nuestra cuenta.

Como resultado, el bloque de trabajo con la base de datos consta de la carpeta «Sqlite3», donde se describe un cómodo envoltorio para trabajar con la base de datos, y la carpeta «OptimisationSelector», creada especialmente para el programa escrito. Ambas carpetas se ubican en el directorio MQL5/Include. Asimismo, al trabajar con la base de datos, como ya hemos mencionado anteriormente, se usa una serie de funciones de la biblioteca estándar de Windows. Todas las funciones de esta parte de la aplicación se ubican en el directorio WinApi. Aparte de los componentes que hemos tomado prestados del CodeBase, se ha usado el código para crear el recurso compartido (Mutex). El recurso compartido se requiere para que, durante el trabajo con una base de datos de dos fuentes (precisamente si el analizador de optimizaciones ha abierto la base de datos implicada durante el proceso de optimización), los datos obtenidos por el programa siempre estén completos. Resulta que si una de las partes (ya sea el proceso de optimización, o el programa analizador) interactúa con la base de datos, la otra esperará a que el trabajo de la anterior finalice. La base de datos Sqlite permite que se la lea desde varios flujos. Debido a la temática del artículo, no vamos a analizar con detalle el envoltorio obtenido como resultado para trabajar con la base de datos sqlite3 de MQL5, sino que vamos a escribir algunos momentos de su implementación y su método de uso. Como ya hemos mencionado, el envoltorio de trabajo con la base de datos se encuentra en la carpeta Sqlite3. En ella se ubican 3 archivos, vamos a echarles un vistazo por orden de escritura.

  • Lo primero que necesitamos es importar la funciones necesarias para trabajar con la base de datos de Dll. Puesto que nuestro objetivo era crear un envoltorio que contuviera la funcionalidad mínima requerida, no hemos importado ni siquiera un 1% de la cantidad total de funciones ofrecidas por los desarrolladores de la base de datos. Todas las funciones necesarias se importan al archivo «sqlite_amalgmation.mqh». Estas funciones se han comentado con detalle en el sitio web del desarrollador, y también se describen en el archivo mencionado más arriba. Si lo deseamos, podemos importar de la misma forma el archivo de encabezado al completo; como resultado, obtendremos una lista completa de todas las funciones y, por consiguiente, acceso a ellas. La lista de funciones importadas es la siguiente:

#import "Sqlite3_32.dll"
int sqlite3_open(const uchar &filename[],sqlite3_p32 &paDb);// Apertura de la base de datos
int sqlite3_close(sqlite3_p32 aDb); // Cierre de la base de datos
int sqlite3_finalize(sqlite3_stmt_p32 pStmt);// Completa la instrucción
int sqlite3_reset(sqlite3_stmt_p32 pStmt); // Redefine la instrucción
int sqlite3_step(sqlite3_stmt_p32 pStmt); // Paso a la siguiente línea al leer la instrucción
int sqlite3_column_count(sqlite3_stmt_p32 pStmt); // Calculando el número de columnas
int sqlite3_column_type(sqlite3_stmt_p32 pStmt,int iCol); // Obteniendo el tipo de columna elegida
int sqlite3_column_int(sqlite3_stmt_p32 pStmt,int iCol);// Convirtiendo el valor en int
long sqlite3_column_int64(sqlite3_stmt_p32 pStmt,int iCol); // Convirtiendo el valor en int64
double sqlite3_column_double(sqlite3_stmt_p32 pStmt,int iCol); // Convirtiendo el valor en double
const PTR32 sqlite3_column_text(sqlite3_stmt_p32 pStmt,int iCol);// Obteniendo valor de texto
int sqlite3_column_bytes(sqlite3_stmt_p32 apstmt,int iCol); // Obteniendo el número de bytes que ocupan la línea de la celda transmitida
int sqlite3_bind_int64(sqlite3_stmt_p32 apstmt,int icol,long a);// Combinando la solicitud con el valor (tipo int64)
int sqlite3_bind_double(sqlite3_stmt_p32 apstmt,int icol,double a);// Combinando la solicitud con el valor (tipo double)
int sqlite3_bind_text(sqlite3_stmt_p32 apstmt,int icol,char &a[],int len,PTRPTR32 destr);// Combinando la solicitud con el valor (tipo string (char* - en C++))
int sqlite3_prepare_v2(sqlite3_p32 db,const uchar &zSql[],int nByte,PTRPTR32 &ppStmt,PTRPTR32 &pzTail);// Preparando la solicitud
int sqlite3_exec(sqlite3_p32 aDb,const char &sql[],PTR32 acallback,PTR32 avoid,PTRPTR32 &errmsg);// Ejecutando Sql
int sqlite3_open_v2(const uchar &filename[],sqlite3_p32 &ppDb,int flags,const char &zVfs[]); // Abriendo la base de datos con los parámetros
#import

Debemos recordar que para el envoltorio funcione, las bases de datos dll ofrecidas por los desarrolladores de la base, deberán encontrarse en la carpeta Libraries y llamarse Sqlite3_32.dll y Sqlite3_64.dll, de acuerdo con su número de bits. Podrá tomar estas Dll de los archivos adjuntos al artículo, compilarlas independientemente desde Sqlite Amalgmation o tomarlas del sitio web de los desarrolladores de Sqlite, usted decide, pero su presencia es obligada para el funcionamiento del programa. Ahora hay que permitirle al experto la importación de Dll.  

  • En segundo lugar, deberemos escribir un envoltorio funcional para conectarse a la base de datos. Debe tratarse de una clase que cree una conexión con la base y la libere (se desconecta de la base) en el destructor. Asimismo, debe saber ejecutar comandos Sql de línea sencillos, gestionar transacciones y crear solicitudes (instrucciones). Toda la funcionalidad descrita se ha implementado en la clase CsqliteManager; precisamente de su creación procede el proceso de interacción con la base de datos.

//+------------------------------------------------------------------+
//| Clase de conexión y gestión de la base de datos                  |
//+------------------------------------------------------------------+
class CSqliteManager
  {
public:
                     CSqliteManager(){db=NULL;} // Constructor vacío
                     CSqliteManager(string dbName); // Transmitiendo nombre
                     CSqliteManager(string dbName,int flags,string zVfs); // Transmitiendo el nombre y las banderas de conexión
                     CSqliteManager(CSqliteManager  &other) { db=other.db; } // Constructor de copiado
                    ~CSqliteManager(){Disconnect();};// Desctructor

   void              Disconnect(); // Desconectando de la base de datos
   bool              Connect(string dbName,int flags,string zVfs); // Conexión parametral a la base de datos
   bool              Connect(string dbName); // Conectando a la base de datos según el nombre

   void operator=(CSqliteManager  &other){db=other.db;}// Operador de asignación

   sqlite3_p64 DB() { return db; }; // Obteniendo el puntero a la base

   sqlite3_stmt_p64  Create_statement(const string sql); // Creando instrucción
   bool              Execute(string sql); // Ejecutando comando
   void              Execute(string  sql,int &result_code,string &errMsg); // Ejecutando el comando y generando el código de error y el mensaje sobre el error

   void              BeginTransaction(); // Comienzo de la transacción
   void              RollbackTransaction(); // Retroceso de la transacción
   void              CommitTransaction(); // Confirmación de la transacción

private:
   sqlite3_p64       db; // Base de datos

   void stringToUtf8(const string strToConvert,// Línea que debemos convertir en matriz en codificación utf-8
                     uchar &utf8[],// Matriz en codificación utf-8 en la que se ubicará la línea strToConvert transformada
                     const bool untilTerminator=true)
     {    // Número de símbolos que se copiarán a la matriz utf8 y que, por consiguiente, serán convertidos a la codificación utf-8
      //---
      int count=untilTerminator ? -1 : StringLen(strToConvert);
      StringToCharArray(strToConvert,utf8,0,count,CP_UTF8);
     }
  };

Como podemos ver por el código, la clase creada tiene la posibilidad de crear dos tipos de conexión a la base (de texto e indicando los parámetros). El método Create_sttement forma la solicitud a la base y retorna el puntero para la instrucción. La sobrecarga del método Exequte es ejecutada por solicitudes de línea sencillas, mientras que los métodos de transacción gestionan la creación y el proceso de toma/cancelación de decisiones. La conexión a la propia base de datos se guarda en db. Si hemos usado el método Disconnect o acabamos de crear la clase con la ayuda del constructor por defecto (es decir, no hemos tenido tiempo de conectarnos a la base), esta variable tendrá el valor NULL. Al llamar de nuevo el método Connect, nos desconectamos de la base conectada anteriormente y nos conectamos a la nueva. Puesto que al conectarnos a la base se necesita la transmisión de la línea en formato UTF-8, en la clase dispondremos de un método private especial, que convierte la línea en el formato de datos necesario.

  • La siguiente tarea consiste en crear un envoltorio para trabajar cómodamente con las solicitudes (statement). La solicitud a la base de datos debe ser creada y eliminada. La creación de la solicitud se ha implementado en CsqliteManager, pero la memoria no la gestiona nadie. En otras palabras, después de crear a solicitud, debemos destruirla cuando ya no sea necesaria; en caso contrario, no nos dejará desviarnos de la base de datos, y al intentar fnalizar el trabajo con la base de datos, nos encontraremos con una excepción que indica que la base está ocupada. Asimismo, la clase-envoltorio de la instrucción debe saber rellenar la solicitud con los parámetros transmitidos (en los casos en los que se transmite según el tipo «INSERT INTO table_1 VALUES(@ID,@Param_1,@Param_2);»). Además, esta clase debe saber ejecutar la solicitud ubicada en ella (método Exequte).

typedef bool(*statement_callback)(sqlite3_stmt_p64); // llamada de retorno realizada al ejecutar la solicitud, en caso de éxito, retorna true
//+------------------------------------------------------------------+
//| Clase de la solicitud a la base de datos                         |
//+------------------------------------------------------------------+
class CStatement
  {
public:
                     CStatement(){stmt=NULL;} // constructor vacío
                     CStatement(sqlite3_stmt_p64 _stmt){this.stmt=_stmt;} // Constructor con el parámetro - puntero a la instrucción
                    ~CStatement(void){if(stmt!=NULL)Sqlite3_finalize(stmt);} // Destructor
   sqlite3_stmt_p64 get(){return stmt;} // Obtener el puntero a la instrucción
   void              set(sqlite3_stmt_p64 _stmt); // Definir el puntero a la instrucción

   bool              Execute(statement_callback callback=NULL); // Ejecutar la instrucción
   bool              Parameter(int index,const long value); // Añadir parámetro
   bool              Parameter(int index,const double value); // Añadir parámetro
   bool              Parameter(int index,const string value); // Añadir parámetro

private:
   sqlite3_stmt_p64  stmt;
  };

Las sobrecargas del método Parameter rellenan los parámetros de la solicitud. El método set guarda el statement transmitido en la variable stmt: si antes de guardar una nueva solicitud resulta que ya se había guardado en la clase una solicitud antigua, se llama el método Sqlite3_finalize para la solicitud anteriormente guardada.

  • Como clase final en el envoltorio de trabajo con la base de datos actúa CSqliteReader, que debe saber leer la respuesta de la base de datos. Asimismo, por analogía con las clases anteriores, esta clase llama en su destructor el método sqlite3_reset, que descarta la solicitud y permite trabajar con él de nuevo. En las nuevas versiones de la base de datos, es posible no llamar esta función, sin embargo, los desarrolladores la han dejado, y la hemos usado por si acaso en el envoltorio presentado. Asimsimo, esta clase debe ejecutar sus principales obligaciones, que son precisamente leer la respuesta de la base de datos línea a línea, con la posibilidad de convertir los datos leídos en el formato correspondiente.
//+------------------------------------------------------------------+
//| Clase que lee las respuestas de la base de datos                 |
//+------------------------------------------------------------------+
class CSqliteReader
  {
public:
                     CSqliteReader(){statement=NULL;} // constructor vacío
                     CSqliteReader(sqlite3_stmt_p64 _statement) { this.statement=_statement; }; // Constructor que recibe el puntero a la instrucción
                     CSqliteReader(CSqliteReader  &other) : statement(other.statement) {} // Constructor de copiado
                    ~CSqliteReader() { Sqlite3_reset(statement); } // Destructor

   void              set(sqlite3_stmt_p64 _statement); // Añadir enlace a la instrucción
   void operator=(CSqliteReader  &other){statement=other.statement;}// Operador de asignación Reader
   void operator=(sqlite3_stmt_p64 _statement) {set(_statement);}// Operador de asignación de la instrucción

   bool              Read(); // Leyendo la línea
   int               FieldsCount(); // Calculando el número de columnas
   int               ColumnType(int col); // Obteniendo el tipo de columna

   bool              IsNull(int col); // Comprobando si el valor es == SQLITE_NULL
   long              GetInt64(int col); // Convirtiendo en int
   double            GetDouble(int col);// Convirtiendo en double
   string            GetText(int col);// Convirtiendo en string

private:
   sqlite3_stmt_p64  statement; // puntero a la instrucción
  };

Tras implementar las clases descritas con la ayuda de las funciones de trabajo con la base de datos descargada de Sqlite3.dll, podemos proceder a describir las clases que trabajan con la base del programa descrito.

La estructura de la base de datos creada es la siguiente:

Recuadro Buy And Hold:

  1. Time — eje X (rótulo de intervalo temporal)
  2. PL_total — beneficio/pérdidas, si incrimentamos el lote en proporción al robot
  3. PL_oneLot — beneficio/pérdidas, si comerciamos todo el tiempo con un lote
  4. DD_total — reducción, si comerciamos con el lote de la misma forma que ha comerciado el robot
  5. DD_oneLot — reducción, si comerciamos con un lote
  6. isForvard — propiedad del gráfico en tiempo real

Recuadro OptimisationParams:

  1. ID — Número único de rellenado automático de entrada en la base de datos
  2. HistoryBorder — Fecha de finalización de la optimización histórica
  3. TF — Marco temporal
  4. Param_1...Param_n — parámetro
  5. InitalBalance — valor del balance inicial

Recuadro ParamsCoefitients:

  1. ID — Clave externa, enlace a OptimisationParams(ID)
  2. isForvard — propiedad de optimización en tiempo real
  3. isOneLot — propiedad del gráfico según el cual se ha calculado el coeficiente
  4. DD — reducción
  5. averagePL — beneficio/pérdidas medios según el gráfico de PL
  6. averageDD — reducción media
  7. averageProfit — beneficio medio
  8. profitFactor — factor de beneficio
  9. recoveryFactor — factor de recuperación
  10. sharpRatio — ratio de Sharpe
  11. altman_Z_Score — Puntuación Z de Altman
  12. VaR_absolute_90 — VaR 90
  13. VaR_absolute_95 — VaR 95
  14. VaR_absolute_99 — VaR 99
  15. VaR_growth_90 — VaR 90
  16. VaR_growth_95 — VaR 95
  17. VaR_growth_99 — VaR 99
  18. winCoef — coeficiente de ganancia
  19. customCoef — coeficiente personalizado

Recuadro ParamType:

  1. ParamName — nombre del parámetro del robot
  2. ParamType — tipo del parámetro del robot (int / double / string)

Recuadro TradingHistory

  1. ID — Clave externa del enlace a OptimisationParams(ID)
  2. isForvard — propiedad de la historia de simulación en tiempo real
  3. Symbol — símbolo
  4. DT_open — fecha de apertura
  5. Day_open — día de apertura
  6. DT_close — fecha de cierre
  7. Day_close — día de cierre
  8. Volume — Número de lotes
  9. isLong — propiedad de long o short
  10. Price_in — precio de entrada
  11. Price_out — precio de salida
  12. PL_oneLot — Beneficio al comerciar con un lote
  13. PL_forDeal — Beneficio al comerciar como se ha hecho anteriormente
  14. OpenComment — comentario de entrada
  15. CloseComment — comentario de salida

Partiendo de la estructura presentada para la base de datos, podemos ver que algunos recuadros se refieren con una clave externa al recuadro OptimisationParams, donde guardamos los parámetros de entrada del robot. Cada columna del parámetro de entrada lleva su nombre (por ejemplo, Fast/Slow — Media móvil rápida/lenta). Asimismo, cada columna debe tener un formato de datos determinado. Muchas bases de Sqlite se crean sin determinar el formato de datos de las columnas en los recuadros, y entonces todos los datos se guardan en forma de líneas, pero para nuestros objetivos, necesitamos saber exactamente el formato de los datos, ya que en lo sucesivo, filtraremos los coeficientes según una propiedad concreta, y, por consiguiente, necesitaremos convertir los datos descargados de la base a su formato original.

Para ello, antes de introducir los datos en la base, deberemos conocer su formato. Podemos actuar de varias formas: o bien crear un método de plantilla y transmitirlo al conversor, o bien crear una clase que, en esencia, será un repositorio universal de varios tipos de datos combinados con el nombre de la variable del robot, a los cuales (hablamos del formato de los datos) se puede transformar prácticamente cualquier tipo de datos. Para implementar la tarea, se ha elegido la segunda variante y se ha creado la clase CDataKeeper. La clase descrita puede guardar 3 tipos de datos [int, double, string], mientras que el resto de tipos de datos que pueden ser formatos de entrada del robot, de una forma u otra pueden ser convertidos a ellos.

//+------------------------------------------------------------------+
//| Tipos de datos de entrada del parámetro del robot                |
//+------------------------------------------------------------------+
enum DataTypes
  {
   Type_INTEGER,// int
   Type_REAL,// double, float
   Type_Text // string
  };
//+------------------------------------------------------------------+
//| Rsultado de la comparación de dos CDataKeeper                    |
//+------------------------------------------------------------------+
enum CoefCompareResult
  {
   Coef_Different,// distintos tipos de datos o nombres de las variables
   Coef_Equal,// las variables son iguales
   Coef_Less, // la variable actual es menor que la transmitida
   Coef_More // la variable actual es mayor que la transmitida
  };
//+---------------------------------------------------------------------+
//| Clase que guarda un parámetro de entrada concreto del robot.        |
//| Puede guardar datos de los siguientes tipos: [int, double, string]  |
//+---------------------------------------------------------------------+
class CDataKeeper
  {
public:
                     CDataKeeper(); // Constructor
                     CDataKeeper(const CDataKeeper&other); // Constructor de copiado
                     CDataKeeper(string _variable_name,int _value); // Constructor paramétrico
                     CDataKeeper(string _variable_name,double _value); // Constructor paramétrico
                     CDataKeeper(string _variable_name,string _value); // Constructor paramétrico

   CoefCompareResult Compare(CDataKeeper &data); // Método de comparación

   DataTypes         getType(){return variable_type;}; // Obteniendo el tipo de datos
   string            getName(){return variable_name;}; // Obteniendo el nombre del parámetro
   string            valueString(){return value_string;}; // Obteniendo el parámetro
   int               valueInteger(){return value_int;}; // Obteniendo el parámetro
   double            valueDouble(){return value_double;}; // Obteniendo el parámetro
   string            ToString(); // Conversión de cualquier parámetro a línea. Si se trata de un parámetros de línea, a esta se le añaden en ambos lados comillas únicas <<'>>

private:
   string            variable_name,value_string; // nombre de la variable y variable de línea
   int               value_int; // variable Int
   double            value_double; // Variable Double
   DataTypes         variable_type; // Tipo de variable

   int compareDouble(double x,double y) // Comparando los tipos Double con una precisión de hasta 10 dígitos
     {
      double diff=NormalizeDouble(x-y,10);
      if(diff>0) return 1;
      else if(diff<0) return -1;
      else return 0;
     }
  };

Las tres sobrecargas del constructor adoptan como primer parámetro el nombre de la variable, y como segundo, el valor convertido a uno de los tipos mencionados. Estos valores se guardan en las variables globales de la clase que comienzan por la palabra value_, y a continuación va la indicación del tipo. El método getType() retorna el tipo adoptando la forma de la enumeración presentada anteriormente, y el método getName(), el nombre de la variable. Los métodos que comienzan con la palabra value retornan la variable del tipo necesario, sin embargo, si se llama el método valueDouble(), y la variable guardada en la clase tiene el tipo int, se retornará NULL. El método ToString() convierte el valor de cualquiera de las variables en formato de línea, sin embargo, si la variable era inicialmente una línea, a esta se le añadirán comillas (para que resulte más cómodo formar las solicitudes SQL). El método Compare(CDataKeeper &ther) ayuda a comparar dos objetos del tipo CDataKeeper, comparando en este caso:

  1. El nombre de la variable del robot
  2. El tipo de la variable
  3. El valor de la variable

Si las dos primeras comparaciones no resultan, significa que hemos intentado comparar dos parámetros distintos (por ejemplo, el periodo de la media móvil rápida con el periodo de la media móvil lenta), y, por consiguiente, no podremos hacer esto, porque necesitamos comparar datos de un mismo tipo. Por eso, retornamos el valor Coef_Different del tipo CoefCompareResult; en los otros casos, tiene lugar la comparación, y luego se retorna el resultado demandado. El método de comparación en sí se implementa de la forma siguiente:

//+------------------------------------------------------------------+
//| Comparando el parámetro actual con el transmitido                |
//+------------------------------------------------------------------+
CoefCompareResult CDataKeeper::Compare(CDataKeeper &data)
  {
   CoefCompareResult ans=Coef_Different;

   if(StringCompare(this. variable_name,data.getName())==0 && 
      this.variable_type==data.getType()) // Comparando el nombre y los tipos
     {
      switch(this.variable_type) // Comparando los resultados
        {
         case Type_INTEGER :
            ans=(this.value_int==data.valueInteger() ? Coef_Equal :(this.value_int>data.valueInteger() ? Coef_More : Coef_Less));
            break;
         case Type_REAL :
            ans=(compareDouble(this.value_double,data.valueDouble())==0 ? Coef_Equal :(compareDouble(this.value_double,data.valueDouble())>0 ? Coef_More : Coef_Less));
            break;
         case Type_Text :
            ans=(StringCompare(this.value_string,data.valueString())==0 ? Coef_Equal :(StringCompare(this.value_string,data.valueString())>0 ? Coef_More : Coef_Less));
            break;
        }
     }
   return ans;
  }

La representación de variables independiente del tipo permite operar con las variables de una forma más cómoda, teniendo en cuenta además tanto el nombre y el tipo de los datos de la variable, como su valor.

La siguiente tarea es la formación de la base de datos descrita más arriba. Con este objetivo, se ha creado la clase CDatabaseWriter.

//+--------------------------------------------------------------------+
//| Llamada de retorno que calcula el coeficiente personalizado        |
//| En la entrada se suministran los datos de la historia y la bandera |
//| para cada tipo de historia se requiere el cálculo del coeficiente  |
//+--------------------------------------------------------------------+
typedef double(*customScoring_1)(const DealDetales &history[],bool isOneLot);
//+----------------------------------------------------------------------+
//| Llamada de retorno que calcula el coeficiente personalizado           |
//| En la entrada se suministra la conexión a la base de datos (solo      |
//| lectura) la historia y la bandera del tipo del coeficiente solicitado |
//+-----------------------------------------------------------------------+
typedef double(*customScoring_2)(CSqliteManager *dbManager,const DealDetales &history[],bool isOneLot);
//+--------------------------------------------------------------------+
//| Clase que guarda los datos en la base y crea la base antes de ello |
//+--------------------------------------------------------------------+
class CDBWriter
  {
public:
   // Llamar en OnInit una de las sobrecargas
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // Llamada de retorno №1
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); // Llamada de retorno №2
   void              OnInitEvent(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT);// Sin llamada de retorno y coeficiente personalizado (igual a cero)
   double            OnTesterEvent();// Llamar en OnTester
   void              OnTickEvent();// Llamar en OnTick

private:
   CSqliteManager    dbManager; // Conector al banco de datos
   CDataKeeper       coef_array[]; // Parámetros de entrada
   datetime          DT_Border; // Fecha de la última vela (se calcula en OnTickEvent)
   double            r; // Tasa libre de riesgo

   customScoring_1   scoring_1; // Llamada de retorno
   customScoring_2   scoring_2; // Llamada de retorno
   int               scoring_type; // Tipo de llamada de retorno [1,2]
   string            DBPath; // Ruta a la base de datos 
   double            balance; // Balance
   ENUM_TIMEFRAMES   TF; // Marco temporal

   void              CreateDB(const string DBPath,const CDataKeeper &inputData_array[],double r,ENUM_TIMEFRAMES TF);// Se crea la base de datos y todo lo que la acompaña
   bool              isForvard();// Definiendo el tipo de optimización actual (histórica / en tiempo real)
   void              WriteLog(string s,string where);// Entrada del archivo log

   int               setParams(bool IsForvard,CReportCreator *reportCreator,DealDetales &history[],double &customCoef);// Rellenando el recuadro de parámetros de entrada
   void              setBuyAndHold(bool IsForvard,CReportCreator *reportCreator);// Rellenando la historia de Buy And Hold
   bool              setTraidingHistory(bool IsForvard,DealDetales &history[],int ID);// Rellenando la historia de transacciones
   bool              setTotalResult(TotalResult &coefData,bool isOneLot,long ID,bool IsForvard,double customCoef);// Rellenando el recuadro con coeficientes
   bool              isHistoryItem(bool IsForvard,DealDetales &item,int ID); // Comprobando si existen o no estos parámetros en el recuadro de la historia de transacciones
  };

Esta clase se usa solo en el propio robot personalizado y debe crear un parámetro de entrada para el programa descrito, más concretamente, la base con la estructura requerida. Como podemos ver, tiene 3 métodos públicos (el método sobrecargado se cuenta como uno:

  • OnInitEvent
  • OnTesterEvent
  • OnTickEvent

Cada uno de ellos se llama en las correspondientes llamadas de retorno de la plantilla del robot, en las cuales se transmiten los parámetros necesarios. El método OnInitEvent prepara la clase para el trabajo con la base de datos, sus sobrecargas se implementan de la forma siguiente:

//+------------------------------------------------------------------+
//| Creando la base de datos y la conexión                           |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_2=scoringFunction;
   scoring_type=2;
  }
//+------------------------------------------------------------------+
//| Creando la base de datos y la conexión                           |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_1=scoringFunction;
   scoring_type=1;
  }
//+------------------------------------------------------------------+
//| Creando la base de datos y la conexión                           |
//+------------------------------------------------------------------+
void CDBWriter::OnInitEvent(const string _DBPath,const CDataKeeper &inputData_array[],double _r,ENUM_TIMEFRAMES _TF)
  {
   CreateDB(_DBPath,inputData_array,_r,_TF);
   scoring_type=0;
  }

Como podemos ver por la implementación de este método, este asigna a los campos de la clase los valores necesarios y crea la base de datos. Los métodos de las llamadas de retorno son implementados por el usuario personalmente (en el caso de que necesitemos calcular nuestro coeficiente), o bien se usa la sobrecarga sin llamada de retorno. En este caso, el coeficiente personalizado es igual a uno. El coeficiente personalizado es un método propio de valoración de la pasada de optimización del robot. Para implementarlo, se han creado punteros a dos funciones con dos tipos de posibles datos requeridos.

  • El primer tipo de ellos, (customScoring_1), obtiene la historia de transacciones y la bandera precisamente para la pasada de optimización de la que se requiere el cálculo (el bloque con el que se comercia realmente, o comercio con un lote: todos los datos para los cálculos se encuentran en la matriz transmitida).
  • El segundo tipo de llamada de retorno (customScoring_2) obtiene acceso a la base de datos desde la que se realiza el trabajo, sin embargo, el acceso se da con los derechos solo de lectura, para evitar correcciones imprevistas por parte del usuario.
El método CreateDB es uno de los principales métodos de clase, este ejecuta la preparación completa para el trabajo:

  • Asigna los valores de balance, marco temporal y tasa sin riesgo.
  • Crea una conexión con la base y ocupa el recurso compartido (Mutex)
  • Crean recuadros en la base, si estos no han sido aún creados.

El método público OnTickEvent en cada tick guarda la fecha de la vela de minuto. El asunto es que durante la simulación de la estrategia no hay posibilidad de determinar si la pasada actual es o no en tiempo real, mientras que en la base de datos existe un parámetro similar. Pero sabemos que el simulador realiza las pasadas en tiempo real después de las históricas. Por consiguiente, al registrar de nuevo en cada tick la variable con la fecha, al final del proceso de optimización conocemos la última fecha de todas. En el recuadro OptimisationParams, existe el parámetro «HistoryBorder», que es precisamente igual a la fecha guardada. Las líneas de este recuadro se introducen solo durante la optimización histórica. Durante la primera pasada con estos parámetros (se trata de una pasada con los datos históricos), la fecha de la que hablamos se introduce en el campo necesario de la base. A continuación, si vemos en una de las siguientes pasadas que la entrada con estos parámetros ya existe en la base, entonces hay dos variantes:

  1. o bien el usuario ha detenido por algún motivo la optimización histórica y la ha iniciado de nuevo,
  2. o bien se trata de la optimización en tiempo real.

Para filtrar lo uno de lo otro, comparamos la última fecha guardada en la pasada actual con la fecha de la base. Si la fecha actual es superior a la de la base, estaremos ante una pasada en tiempo real; si es igual o inferior, estaremos ante pasada histórica. Teniendo en cuenta que la optimización puede iniciarse dos veces con los mismos coeficientes, introducimos en la base solo los datos nuevos, o bien cancelamos todos los cambios realizados en la pasada actual. El método OnTesterEvent() guarda los datos en la base. Se ha implementado de la forma siguiente:

//+------------------------------------------------------------------+
//| Guardando todos los datos en la base y retornando                |
//| el coeficiente personalizado                                     |
//+------------------------------------------------------------------+
double CDBWriter::OnTesterEvent()
  {

   DealDetales history[];

   CDealHistoryGetter historyGetter;
   historyGetter.getDealsDetales(history,0,TimeCurrent()); // Obteniendo la historia de transacciones

   CMutexSync sync; // objeto de sincronización en sí 
   if(!sync.Create(getMutexName(DBPath))) { Print(Symbol()+" MutexSync create ERROR!"); return 0; }
   CMutexLock lock(sync,(DWORD)INFINITE); // bloqueamos el segmento dentro de estos paréntesis

   bool IsForvard=isForvard(); // Descubriendo si la iteración actual del simulador es en tiempo real
   CReportCreator rc;
   string Symb[];
   rc.Get_Symb(history,Symb); // Obteniendo la lista de símbolos
   rc.Create(history,Symb,balance,r); // Creando un informe (el informe Buy And Hold se crea de forma independiente)

   double ans=0;
   dbManager.BeginTransaction(); // Iniciando transacción

   CStatement stmt(dbManager.Create_statement("INSERT OR IGNORE INTO ParamsType VALUES(@ParamName,@ParamType);")); // Solicitando el guardado de la lista de tipos de parámetros del robot
   if(stmt.get()!=NULL)
     {
      for(int i=0;i<ArraySize(coef_array);i++)
        {
         stmt.Parameter(1,coef_array[i].getName());
         stmt.Parameter(2,(int)coef_array[i].getType());
         stmt.Execute(); // guardamos los tipos de parámetros y sus denominaciones
        }
     }

   int ID=setParams(IsForvard,&rc,history,ans); // Guardamos los parámetros del robot, los coeficientes de valoración y obtenemos la ID
   if(ID>0)// Si ID > 0, los parámetros se han guardado con éxito 
     {
      if(setTraidingHistory(IsForvard,history,ID)) // Guardamos la historia comercial y comprobamos si se ha guardado
        {
         setBuyAndHold(IsForvard,&rc); // Guardamos la historia Buy And Hold (se guardará solo una vez, durante el primer guardado)
         dbManager.CommitTransaction(); // Confirmamos la finalización de la transacción
        }
      else dbManager.RollbackTransaction(); // De lo contrario, cancelamos la transacción
     }
   else dbManager.RollbackTransaction(); // De lo contrario, cancelamos la transacción

   return ans;
  }

Lo primero que hace el método es formar la historia de transacciones usando la clase descrita en nuestro anterior artículo. A continuación, ocupa el recurso compartido (Mutex) y guarda los datos. Para ello, se determina por primera vez si la pasada de optimización acutual es en tempo real (según el método descrito arriba), después obtenemos la lista de símbolos (todos con los que se ha comerciado).

Por consiguiente, si se ha simulado un asesor que comercie, por ejemplo, con spread, se descargará la historia de transacciones de los símbolos con los que se ha realizado el comercio. A continuación, se crea un informe con la ayuda de la clase que veremos más abajo, y se realiza el registro en la base de datos. Para realizar correctamente el registro, se crea una transacción que se cancelará en el caso de que suceda un error al rellenar algún recuadro, o bien se obtengan datos incorrectos. Primero se guardan los coeficientes, y después, si todo ha pasado bien, guardamos la historia comercial, a continuación, la historia de "Buy And Hold", pero esta se guarda solo una vez al introducir los datos por primera vez. En caso de error al guardar los datos, se forma un Log File en la carpeta Common/Files.

Después de crear la base de datos, deberemos leerla: la clase de lectura de la base ya se utiliza en el programa usado. Es más sencilla, y se representa de la forma siguiente:

//+------------------------------------------------------------------+
//| Clase que lee los datos desde la base                            |
//+------------------------------------------------------------------+
class CDBReader
  {
public:
   void              Connect(string DBPath);// Método que se conecta a la base

   bool              getBuyAndHold(BuyAndHoldChart_item &data[],bool isForvard);// Método que lee la historia de Buy And Hold 
   bool              getTraidingHistory(DealDetales &data[],long ID,bool isForvard);// Método que lee la historia comerciada por el robot 
   bool              getRobotParams(CoefData_item &data[],bool isForvard);// Método que lee los parámetros del robot y los coeficientes

private:
   CSqliteManager    dbManager; // Gestor de la base de datos 
   string            DBPath; // Ruta a la basae

   bool              getParamTypes(ParamType_item &data[]);// Lee los tipos de los parámetros de entrada y sus nombres.
  };

En ella se han implementado 3 métodos públicos que leen los 4 recuadros que nos interesan y que crean las matrices de las estructuras con los datos de estos recuadros.

  • El primer método, getBuyAndHold, retorna según un enlace la historia de BuyAndHold para los periodos histórico y en tiempo real, dependiendo de la bandera transmitida. Si la descarga ha tenido éxito, el método retorna true, de lo contrario, false. La descarga se realiza desde el recuadro Buy And Hold.
  • El método getTradingHistory también retorna la historia comercial para la ID transmitida, y la bandera isForvard, respectivamente. La descarga se realiza desde el recuadro TradingHistory.
  • El método getRobotParams combina la descarga de los dos recuadros: ParamsCoefitients, desde donde se toman los parámetros del robot, y OptimisationParams, donde se encuentran los coeficientes de valoración calculados.

De esta forma, las clases escritas permiten trabajar directamente no con la base de datos, sino con las clases que proporcionan los datos necesarios, ocultando todo el algoritmo de trabajo con la base. Estas clases, a su vez, trabajan con el envoltorio escrito para la base de datos, lo que también simplifica el trabajo con él. El envoltorio mencionado trabaja con la base a través de una Dll proporcionada por los desarrolladores de la base de datos. La propia base responde a todas las solicitudes exigidas y, en realidad, supone un archivo cómodo de transportar y procesar tanto en este programa, como en otros programas de análisis. Otra ventaja de este enfoque es el hecho de que cuando el algoritmo trabaja por un periodo largo, es posible recopilar una base de datos de cada optimización (acumulando también una historia) y monitorear las tendencias de cambio de los parámetros.


Cálculos

Este bloque consta de dos clases. La primera clase crea el informe de transacciones. Se trata de una versión mejorada de la clase que crea el informe de transacciones que hemos visto en el artículo anterior.

La otra clase es la clase del filtro. Esta clase filtra la muestra de optimización en el intervalo indicado, y también puede crear un gráfico de frecuencia de las transacciones con beneficio o pérdidas para cada valor por separado del coeficiente de optimización. Otra función de esta clase es la creación de un gráfico de distribución normal según el PL comerciado real al final de la optimización (es decir, el PL del periodo de optimiziación completo). Dicho con otras palabras: si hay 1000 pasadas de optimización, tendremos 1000 resultados de optimización (PL al final de la optimización). Precisamente en función de estos se construye la distribución que nos interesa.

Esta distribución muestra hacia qué lado está desplazada la asimetría de los valores, si la parte mayor y el centro se encuentran en la zona de beneficios, el robot generará más pasadas rentables de optimización, y por consiguiente, será bueno; en el caso contrario, generará más pasadas con pérdidas. Si la asimetría de definición está desplazada hacia la zona de pérdidas, esto también significa que los parámetros elegidos probablemente traigan más pérdidas que beneficios.

Vamos a comenzar el análisis de este bloque por orden, a partir de la clase que crea el informe de transacciones. La clase descrita se encuentra en el directorio Include, en la carpeta «History manager», que tiene el siguiente encabezado:

//+-------------------------------------------------------------------+
//| Clase para crear las estadísticas de la historia de transacciones |
//+-------------------------------------------------------------------+
class CReportCreator
  {
public:

   //=============================================================================================================================================
   // Calculation/ Recalculation:
   //=============================================================================================================================================

   void              Create(DealDetales &history[],DealDetales &BH_history[],const double balance,const string &Symb[],double r);
   void              Create(DealDetales &history[],DealDetales &BH_history[],const string &Symb[],double r);
   void              Create(DealDetales &history[],const string &Symb[],const double balance,double r);
   void              Create(DealDetales &history[],double r);
   void              Create(const string &Symb[],double r);
   void              Create(double r=0);

   //=============================================================================================================================================
   // Getters:
   //=============================================================================================================================================

   bool              GetChart(ChartType chart_type,CalcType calc_type,PLChart_item &out[]); // Obteniendo los gráficos de PL
   bool              GetDistributionChart(bool isOneLot,DistributionChart &out); // Obteniendo los gráficos de distribución
   bool              GetCoefChart(bool isOneLot,CoefChartType type,CoefChart_item &out[]); // Obteniendo los gráficos de los coeficientes
   bool              GetDailyPL(DailyPL_calcBy calcBy,DailyPL_calcType calcType,DailyPL &out); // Obteniendo el gráfico de PL por días
   bool              GetRatioTable(bool isOneLot,ProfitDrawdownType type,ProfitDrawdown &out); // Obteniendo el recuadro de los puntos extremos 
   bool              GetTotalResult(TotalResult &out); // Obteniendo el recuadro de TotalResult
   bool              GetPL_detales(PL_detales &out); // Obteniendo el recuadro de PL_detales
   void              Get_Symb(const DealDetales &history[],string &Symb[]); // Obteniendo la matriz de los instrumentos que han participado en las transacciones
   void              Clear(); // Limpiando estadísticas

private:
   //=============================================================================================================================================
   // Private data types:
   //=============================================================================================================================================
   // Estructura de los tipos del gráfico de PL
   struct PL_keeper
     {
      PLChart_item      PL_total[];
      PLChart_item      PL_oneLot[];
      PLChart_item      PL_Indicative[];
     };
   // Estructura de los tipos del gráfico diario de Beneficio/Pérdidas
   struct DailyPL_keeper
     {
      DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
     };
   // Estructura de los recuadros de puntos extremos
   struct RatioTable_keeper
     {
      ProfitDrawdown    Total_max,Total_absolute,Total_percent;
      ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
     };
   // Estructura para el cálculo de la cantidad de beneficios y pérdidas consecutivos
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Estructura para el cálculo de los datos auxiliares
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };
   // Estructura para crear los gráficos de los coeficientes
   struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };
   // Clase que participa en la clasificación de la historia de transacciones por fecha de cierre.
   class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };
   //=============================================================================================================================================
   // Keepers:
   //=============================================================================================================================================
   CHistoryComparer  historyComparer; // Сравнивающий класс
   CChartComparer    chartComparer; // Clase comparativa

                                    // Estructuras auxiliares
   PL_keeper         PL,PL_hist,BH,BH_hist;
   DailyPL_keeper    DailyPL_data;
   RatioTable_keeper RatioTable_data;
   TotalResult       TotalResult_data;
   PL_detales        PL_detales_data;
   DistributionChart OneLot_PDF_chart,Total_PDF_chart;
   CoefChart_keeper  CoefChart_data;

   double            balance,r; // Depósito inicial de la tasa sin riesgo
                                // Clase de clasificador
   CGenericSorter    sorter;

   //=============================================================================================================================================
   // Calculations:
   //=============================================================================================================================================
   // Cálculo de PL
   void              CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // Cálculo de los Histogramas de PL
   void              CalcPLHist(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type);
   // Cálculo de las estructuras auxiliares con las que se construye todo
   void              CalcData(const DealDetales &deal,CalculationData &out,bool isBH);
   void              CalcData_item(const DealDetales &deal,CalculationData_item &out,bool isOneLot);
   // Cálculo del beneficio/pérdidas diarios
   void              CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal);
   void              cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy);
   void              avarageDay(PLDrawdown &day);
   // Comparando los símbolos
   bool              isSymb(const string &Symb[],string symbol);
   // Calculando el Factor de Beneficio
   void              ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Calculando el Factor de Recuperación
   void              RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Calculando Ratio de Ganancia
   void              WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot);
   // Calculando Ratio de Sharpe
   double            ShartRatio_calc(PLChart_item &data[]);
   void              ShartRatio_chart_calc(CoefChart_item &out[],PLChart_item &data[],const DealDetales &deal);
   // Calculando distribución
   void              NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]);
   double            PDF_calc(double Mx,double Std,double x);
   // Calculando VaR
   double            VaR(double quantile,double Mx,double Std);
   // Calculando puntuación Z 
   void              AltmanZScore_chart_calc(CoefChart_item &out[],double N,double R,double W,double L,const DealDetales &deal);
   // Calculando la estructura TotalResult_item
   void              CalcTotalResult(CalculationData &data,bool isOneLot,TotalResult_item &out);
   // Calculando la estructura PL_detales_item
   void              CalcPL_detales(CalculationData_item &data,int deals_num,PL_detales_item &out);
   // Obteniendo el día de la fecha
   ENUM_DAY_OF_WEEK  getDay(datetime DT);
   // Limpiando los datos
   void              Clear_PL_keeper(PL_keeper &data);
   void              Clear_DailyPL(DailyPL &data);
   void              Clear_RatioTable(RatioTable_keeper &data);
   void              Clear_TotalResult_item(TotalResult_item &data);
   void              Clear_PL_detales(PL_detales &data);
   void              Clear_DistributionChart(DistributionChart &data);
   void              Clear_CoefChart_keeper(CoefChart_keeper &data);

   //=============================================================================================================================================
   // Copy:
   //=============================================================================================================================================
   void              CopyPL(const PLChart_item &src[],PLChart_item &out[]); // Copiando los gráficos de PL
   void              CopyCoefChart(const CoefChart_item &src[],CoefChart_item &out[]); // Copiando los gráficos de los coeficientes

  };

Esta clase, a diferencia de su anterior versión, calcula el doble de datos y construye más tipos de gráficos. Asimismo, las sobrecargas del método Create calculan el informe.

En esencia, el informe se genera solo una vez, en el momento de la llamada del método Create, y después, en los métodos que comienzan con la palabra Get, solo se obtienen los datos calculados anteriormente. El ciclo principal, que itera los parámetros de entrada una sola vez, se ubica en el método Create con el mayor número de argumentos. Este método itera un argumento tras otro y calcula de inmediato una serie de datos según los cuales en la misma iteración se contruyen todos los datos necesarios.

Esto permite construir todo lo que deseamos en una pasada por la matriz de datos, al tiempo que la anterior versión de esta clase iteraba de nuevo los datos originales para obtener cada gráfico. El resultado es que el cálculo de todos los coeficientes dura milisegundos, mientras que la obtención de los datos necesarios dura aún menos. En la zona private de esta clase se ha creado una serie de estructuras que se usan solo dentro de esta clase, y que sirven como contenedores de datos de forma más cómoda. La clasificación de la historia de transacciones y los otros gráficos se realiza con la ayuda del método de clasificación Generic, descrito más arriba.

Vamos a describir los datos obtenidos al llamar cada uno de los getter:

Método Parámetros tipo de gráfico
GetChart chart_type = _PL, calc_type = _Total Gráfico de PL — según la historia comercial real
GetChart chart_type = _PL, calc_type = _OneLot Gráfico de PL — al comerciar con un lote
GetChart chart_type = _PL, calc_type = _Indicative Gráfico de PL — indicativo
GetChart chart_type = _BH, calc_type = _Total Gráfico de BH — si se gestiona el lote como el robot
GetChart chart_type = _BH, calc_type = _OneLot Gráfico de BH — si se comercia con un lote
GetChart chart_type = _BH, calc_type = _Indicative Gráfico de BH — indicativo
GetChart chart_type = _Hist_PL, calc_type = _Total Histograma de PL — según la historia comercial real
GetChart chart_type = _Hist_PL, calc_type = _OneLot Histograma de BH — si se comercia con un lote
GetChart chart_type = _Hist_PL, calc_type = _Indicative Histograma de PL — indicativo
GetChart chart_type = _Hist_BH, calc_type = _Total Histograma de BH — si se gestiona el lote como el robot
GetChart chart_type = _Hist_BH, calc_type = _OneLot Histograma de BH — si se comercia con un lote
GetChart chart_type = _Hist_BH, calc_type = _Indicative Histograma de PL — indicativo
GetDistributionChart isOneLot = true Distribuciones y VaR al comerciar con un lote
GetDistributionChart isOneLot = false Distribuciones y VaR al comerciar como lo hemos hecho anteriormente
GetCoefChart isOneLot = true, type=_ShartRatio_chart Ratio de Sharpe por tiempo al comerciar con un lote
GetCoefChart isOneLot = true, type=_WinCoef_chart Coeficiente de ganancia por tiempo al comerciar con un lote
GetCoefChart isOneLot = true, type=_RecoveryFactor_chart Factor de recuperación por tiempo al comerciar con un lote
GetCoefChart isOneLot = true, type=_ProfitFactor_chart Factor de beneficio por tiempo al comerciar con un lote
GetCoefChart isOneLot = true, type=_AltmanZScore_chart Puntuación Z de Altman por tiempo al comerciar con un lote
GetCoefChart isOneLot = false, type=_ShartRatio_chart Ratio de Sharpe por tiempo al comerciar como lo hemos hecho anteriormente
GetCoefChart isOneLot = false, type=_WinCoef_chart Coeficiente de ganancia por tiempo al comerciar como lo hemos hecho anteriormente
GetCoefChart isOneLot = false, type=_RecoveryFactor_chart Factor de recuperación por tiempo al comerciar como lo hemos hecho anteriormente
GetCoefChart isOneLot = false, type=_ProfitFactor_chart Factor de beneficio por tiempo al comerciar como lo hemos hecho anteriormente
GetCoefChart isOneLot = false, type=_AltmanZScore_chart Puntuación Z de Altman por tiempo al comerciar como lo hemos hecho anteriormente
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=AVERAGE_DATA PL medio por días en el momento de cierre
GetDailyPL calcBy=CALC_FOR_CLOSE, calcType=ABSOLUTE_DATA PL total por días en el momento de cierre
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=AVERAGE_DATA PL medio por días en el momento de apertura
GetDailyPL calcBy=CALC_FOR_OPEN, calcType=ABSOLUTE_DATA PL total por días en el momento de apertura
GetRatioTable isOneLot = true, type = _Max Si comerciamos con un lote — beneficio/pérdidas máximos alcanzados en una transacción
GetRatioTable isOneLot = true, type = _Absolute Si comerciamos con un lote — beneficio/pérdidas totales
GetRatioTable isOneLot = true, type = _Percent Si comerciamos con un lote — cantidad beneficio/pérdidas en %
GetRatioTable isOneLot = false, type = _Max Si comerciamos como lo hemos hecho anteriormente — beneficio/pérdidas máximos alcanzados en una transacción
GetRatioTable isOneLot = false, type = _Absolute Si comerciamos como lo hemos hecho anteriormente — beneficio/pérdidas totales
GetRatioTable isOneLot = false, type = _Percent Si comerciamos como lo hemos hecho anteriormente — cantidad de beneficio/pérdidas en %
GetTotalResult
Recuadro con los coeficientes
GetPL_detales
Breve resumen de la curva de PL
Get_Symb
Matriz de símbolos que había en la historia comercial

Gráfico de PL — según la historia comercial real:

Este gráfico es comparable al gráfico de PL habitual que vemos en el terminal al finalizar todas las pasadas del simulador.

Gráfico de PL — al comerciar con un lote:

Este gráfico es semejante al anteriormente descrito, sin embargo, se diferencia en el volumen comerciado. Se calcula como si hubiéramos comerciado todo el tiempo con un volumen de un lote. Los precios de entrada y salida se calculan como la promediación del precio según el número total de entradas del robot a la posición y la salida de la misma. El beneficio de la transacción se calcula a partir del beneficio que ha sido comerciado por el robot, sin embargo, con la ayuda de la proporción, se convierte en el beneficio del comercio con un lote.

Gráfico de PL — indicativo:

Gráfico de PL normalizado. Si PL > 0, entonces PL se divide por la transacción con mayores pérdidas alcanzada hasta el momento, de lo contrario, PL se divide por la transacción con mayor beneficio alcanzada hasta el momento.

Los gráficos de los histogramas se construyen de forma semejante.

Distribuciones y VaR

VaR - de forma paramétrica, se construye ya sea de acuerdo con los datos absolutos, o de acuerdo con los incrementos.

El gráfico de distribución también se construye ya sea de acuerdo con los datos absolutos, o de acuerdo con los incrementos.

Gráficos de los coeficientes:

Se construyen en cada iteración del ciclo de acuerdo con las fórmulas correspondientes, según la historia disponible en una iteración concreta.

Gráficos de beneficio diario:

Se construyen según las 4 combinaciones de beneficio mencionadas en el recuadro. Tiene forma de histograma.

El método que crea todos los datos enumerados tiene el aspecto siguiente:

//+------------------------------------------------------------------+
//| Cálculo / Recálculo de coeficientes                              |
//+------------------------------------------------------------------+
void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r)
  {
   Clear(); // Limpiando los datos
            // Guardando el balance
   this.balance=_balance;
   if(this.balance<=0)
     {
      CDealHistoryGetter dealGetter;
      this.balance=dealGetter.getBalance(history[ArraySize(history)-1].DT_open);
     }
   if(this.balance<0)
      this.balance=0;
// Guardando la tasa sin riesgo
   if(_r<0) _r=0;
   this.r=r;

// Estructuras auxiliares
   CalculationData data_H,data_BH;
   ZeroMemory(data_H);
   ZeroMemory(data_BH);
// Clasificando la historia de transacciones
   sorter.Method(Sort_Ascending);
   sorter.Sort<DealDetales>(history,&historyComparer);
// Ciclo por la historia de transacciones
   for(int i=0;i<ArraySize(history);i++)
     {
      if(isSymb(Symb,history[i].symbol))
         CalcData(history[i],data_H,false);
     }
// Clasificando la historia de Buy And Hold y el ciclo por la misma
   sorter.Sort<DealDetales>(BH_history,&historyComparer);
   for(int i=0;i<ArraySize(BH_history);i++)
     {
      if(isSymb(Symb,BH_history[i].symbol))
         CalcData(BH_history[i],data_BH,true);
     }

// promediamos los PL diarios (de tipo promediado)
   avarageDay(DailyPL_data.avarage_close.Mn);
   avarageDay(DailyPL_data.avarage_close.Tu);
   avarageDay(DailyPL_data.avarage_close.We);
   avarageDay(DailyPL_data.avarage_close.Th);
   avarageDay(DailyPL_data.avarage_close.Fr);

   avarageDay(DailyPL_data.avarage_open.Mn);
   avarageDay(DailyPL_data.avarage_open.Tu);
   avarageDay(DailyPL_data.avarage_open.We);
   avarageDay(DailyPL_data.avarage_open.Th);
   avarageDay(DailyPL_data.avarage_open.Fr);

// Rellenamos el recuadro de proporción de beneficio y pérdidas
   RatioTable_data.data_H.oneLot.Accomulated_Profit;
   RatioTable_data.data_H.oneLot.Accomulated_DD;
   RatioTable_data.data_H.oneLot.Max_Profit_forDeal;
   RatioTable_data.data_H.oneLot.Max_DD_forDeal;
   RatioTable_data.data_H.oneLot.Total_Profit_numDeals/data_H.num_deals;
   RatioTable_data.data_H.oneLot.Total_DD_numDeals/data_H.num_deals;

   RatioTable_data.Total_absolute.Profit=data_H.total.Accomulated_Profit;
   RatioTable_data.Total_absolute.Drawdown=data_H.total.Accomulated_DD;
   RatioTable_data.Total_max.Profit=data_H.total.Max_Profit_forDeal;
   RatioTable_data.Total_max.Drawdown=data_H.total.Max_DD_forDeal;
   RatioTable_data.Total_percent.Profit=data_H.total.Total_Profit_numDeals/data_H.num_deals;
   RatioTable_data.Total_percent.Drawdown=data_H.total.Total_DD_numDeals/data_H.num_deals;

// Calculando la distribución normal
   NormalPDF_chart_calc(OneLot_PDF_chart,PL.PL_oneLot);
   NormalPDF_chart_calc(Total_PDF_chart,PL.PL_total);

// TotalResult
   CalcTotalResult(data_H,true,TotalResult_data.oneLot);
   CalcTotalResult(data_H,false,TotalResult_data.total);

// PL_detales
   CalcPL_detales(data_H.oneLot,data_H.num_deals,PL_detales_data.oneLot);
   CalcPL_detales(data_H.total,data_H.num_deals,PL_detales_data.total);
  }

Como podemos ver por su implementación, una parte de los datos se calcula a medida que transcurre el ciclo por la historia, y otra parte, después de todos los ciclos basados en los datos de las estructuras: CalculationData data_H,data_BH.

El método CalcData se ha implementado de una forma semejante a Create. Solo él llama aquellos métodos que deberán encargarse de los cálculos en cada iteración. Todos los métodos que calculan los datos finales, los calculan partiendo de la información contenida en las estructuras mencionadas. El rellenado/reabastecimiento de las estructuras descritas se realiza con el método siguiente:

//+------------------------------------------------------------------+
//| Calculando los datos auxiliares                                  |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); // PL
   int n=0;
// Cantidad de beneficios y pérdidas
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Serie de beneficios y pérdidas
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; // PL total
               // Макс Profit / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Profit / DD acumulado
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Puntos de extremo según el beneficio
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// para guardar la fecha será necesaria otra comprobación
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)DD=out.PL-maxPL;
   else DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// para guardar la fecha será necesaria otra comprobación
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

Se trata del método principal que calcula todos los datos originales para cada uno de los métodos de cálculo. Precisamente este enfoque (el traslado de los cálculos de los datos originales a este método) permite evitar un número excesivo de pasadas en los ciclos por la historia, hecho que tenía lugar en la versión anterior de la clase que crea el informe de transacciones. Este método se llama dentro del método CalcData.

La clase del filtro de resultados de la pasada de optimización tiene el siguiente encabezado:

//+-----------------------------------------------------------------------------------+
//| Clase que filtra las pasadas de optimización después de su descarga desde la base.|
//+-----------------------------------------------------------------------------------+
class CParamsFiltre
  {
public:
                     CParamsFiltre(){sorter.Method(Sort_Ascending);} // Constructor por defecto. 
   int               Total(){return ArraySize(arr_main);}; // Número total de parámetros descargados (según el recuadro Optimisation Data)
   void              Clear(){ArrayFree(arr_main);ArrayFree(arr_result);}; // Limpiando todas las matrices
   void              Add(LotDependency_item &customCoef,CDataKeeper &params[],long ID,double total_PL,bool addToResult); // Añadiendo un nuevo valor a la matriz
   double            GetCustomCoef(long ID,bool isOneLot);// Obteniendo el coeficiente personalizado según la ID
   void              GetParamNames(CArrayString &out);// Obteniendo la denominación de los parámetros del robot
   void              Get_UniqueCoef(UniqCoefData_item &data[],string paramName,CArrayString &coefValue); // Obteniendo los coeficientes únicos
   void              Filtre(string Name,string from,string till,long &ID_Arr[]);// filtrando la matriz arr_result
   void              ResetFiltre(long &ID_arr[]);// Resetenado el filtro

   bool              Get_Distribution(Chart_item &out[],bool isMainTable);// Construyendo la distribución según ambas matrices
   bool              Get_Distribution(Chart_item &out[],string Name,string value);// Construyendo distribución según los datos dados

private:
   CGenericSorter    sorter; // Clasificador
   CCoefComparer     cmp_coef;// Comparando coeficientes
   CChartComparer    cmp_chart;// Comparando gráficos

   bool              selectCoefByName(CDataKeeper &_input[],CDataKeeper &out,string Name);// Eligiendo coeficientes por nombre
   double            Mx(CoefStruct &_arr[]);// Media aritmética
   double            Std(CoefStruct &_arr[],double _Mx);// Desviación típica

   CoefStruct        arr_main[]; // Análogo del recuadro Optimisation data
   CoefStruct        arr_result[];// Análogo del recuadro Result
  };

Vamos a analizar la estructura de la clase y hablar con mayor detalle sobre algunos de los métodos. Como podemos ver, la clase tiene dos matrices globales: arr_main y arr_result. Estas matrices son repositorios de estas optimizaciones. Después de descargar el recuadro con las pasadas de la base, este se dividirá en dos recuadros:

  • main — aquí entran todos los datos descargados, excepto aquellos que han sido descartados teniendo en cuenta el filtrado condicional
  • result — entran los n mejores datos elegidos inicialmente. Después de ello, la clase descrita filtra precisamente el recuadro y, propiamente, reduce o bien resetea a su estado original el número de entradas en este recuadro.

Las matrices descritas guardan la ID, los parámetros del robot y otros datos de los recuadros mencionados de acuerdo con el nombre de las matrices. En esencia, esta clase ejecuta dos funciones: es un cómodo repositorio de datos para las operaciones con los recuadros, y también filtra el recuadro de los resultados de las pasadas de optimización elegidas. La clase de clasificación y las dos clases comparativas participan en el proceso de clasificación de las matrices mencionadas, así como en la clasificación de las distribuciones que se construyen según los recuadros descritos.

Puesto que esta clase opera con los coeficientes de los robots, y precisamente con su representación en forma de clase CdataKeeper, se ha creado el método privado «selectCoefByName». Este selecciona el coeficiente necesario y retorna el resultado según el enlace de la matriz de coeficientes transmitidos de una pasada de optimización concreta.

El método Add añade una línea descargada desde la base de datos a ambas matrices (con la condición de que el parámetro addToResult ==true) o solo a la matriz arr_main (si addToResult ==false). ID — es un parámetro único de cada pasada de optimización, por eso, según este se construye todo el trabajo con la definición de una pasada concreta seleccionada. Según este parámetro, obtenemos de las matrices presentadas el coeficiente calculado por el usuario. El programa en sí no conoce la fórmula de cálculo de la valoración personalizada, puesto que la valoración se calcula durante la optimización del robot, sin la participación de este programa. Precisamente por ello estamos obligados a guardar la valoración personalizada en los datos de la matriz y, cuando esta se solicite, obtenerla con la ayuda del método GetCustomCoef según la ID transmitida.

Los métodos más importantes de la clase son los siguientes:

  • Filtre — filtra el recuadro de resultados de tal forma que en él entren los valores del coeficiente elegido en el intervalo transmitido (from/till).
  • ResetFiltre — resetea toda la información filtrada.
  • Get_Distribution(Chart_item &out[],bool isMainTable)— construye una distribución según el PL comerciado realmente de acuerdo con el recuadro elegido, indicado con la ayuda del parámetro isMainTable.
  • Get_Distribution(Chart_item &out[],string Name,string value) — crea una nueva matriz, donde el parámetro elegido (Name) es igual al valor transmitido (value). Dicho con otras palabras, se realiza una pasada en el ciclo según la matriz arr_result. Durante cada iteración del ciclo — de todos los parámetros del robot — se elige el parámetro que nos interese por su nombre (con la ayuda de la función selectCoefByName) y se comprueba si su valor es igual al necesario (value). Si es igual, este valor de la matriz arr_result se introduce en la matriz temporal. A continuación, según la matriz temporal, se construye la distribución y se retorna. Dicho de otra forma, de esta manera, seleccionamos todas las pasadas de optimización donde se ha encontrado un valor elegido por su nombre igual al valor transmitido. Esto es necesario para valorar el grado de influencia de este parámetro concreto en el robot en general. La implementación de la clase descrita se ha comentado lo suficiente en el código, por eso no vamos a mostrar la implementación de estos métodos dentro del texto del artículo.


El "Presenter"

El presenter actúa como conector. Se trata de un nexo entre la aplicación gráfica y su lógica descritas más arriba. En esta aplicación, el presenter se ha implementado con la ayuda de una abstracción: la interfaz IPresenter. Esta interfaz contiene el encabezado de los métodos de las llamadas de retorno necesarias. Estas, a su vez, se implementan en la clase del presenter, que debe heredar la interfaz necesaria. Esta división se ha creado para que exista la posibilidad de mejorar la aplicación. Si necesitamos reescribir el bloque del presenter, podremos hacerlo sin tocar el bloque gráfico o la lógica de la aplicación. La interfaz descrita se muestra de la forma siguiente:

//+------------------------------------------------------------------+
//| Interfaz del presenter                                           |
//+------------------------------------------------------------------+
interface IPresenter
  {
   void Btn_Update_Click(); // Descargando los datos y construyendo el formulario completo
   void Btn_Load_Click(); // Creando el informe
   void OptimisationData(bool isMainTable);// Eligiendo la línea de optimización en los recuadros 
   void Update_PLByDays(); // Descargando el beneficio y las pérdidas por días
   void DaySelect();// Eligiendo el Día del recuadro de PL por días
   void PL_pressed(PLSelected_type type);// Construyendo el gráfico de PL según la historia elegida 
   void PL_pressed_2(bool isRealPL);// Construyendo los gráficos "Other charts"
   void SaveToFile_Click();// Guardando el archivo con los datos (en el sandbox)
   void SaveParam_passed(SaveParam_type type);// Eligiendo los datos para registrar en el archivo
   void OptimisationParam_selected(); // Eligiendo el parámetro de optimización y rellenando la pestaña "Optimisation selection"
   void CompareTables(bool isChecked);// Construyendo la distribución según el recuadro con los resultados (para que se corresponda con el recuadro general(principal))
   void show_FriquencyChart(bool isChecked);// Mostrando el gráfico de frecuencias de beneficio/pérdidas
   void FriquencyChart_click();// Eligiendo la línea en el recuadro con los coeficientes y construyendo la distribución
   void Filtre_click();// Filtrando según las condiciones elegidas
   void Reset_click();// Reseteando filtros
   void PL_pressed_3(bool isRealPL);// Construyendo los gráficos de beneficio/pérdidas según todos los datos del recuadro Result
   void PL_pressed_4(bool isRealPL);// Construyendo los recuadros con las estadísticas
   void setChartFlag(bool isPlot);// Condición para construir (o no construir) el gráfico del método PL_pressed_3(bool isRealPL);
  };

La clase del presenter implementa la interfaz necesaria y tiene el aspecto siguiente:

class CPresenter : public IPresenter
  {
public:
                     CPresenter(CWindowManager *_windowManager); // Constructor

   void              Btn_Update_Click();// Descargando los datos y construyendo el formulario completo
   void              Btn_Load_Click();// Creando el informe
   void              OptimisationData(bool isMainTable);// Eligiendo la línea de optimización en los recuadros 
   void              Update_PLByDays();// Descargando el beneficio y las pérdidas por días
   void              PL_pressed(PLSelected_type type);// Construyendo el gráfico de PL según la historia elegida 
   void              PL_pressed_2(bool isRealPL);// Construyendo los gráficos "Other charts"
   void              SaveToFile_Click();// Guardando el archivo con los datos (en el sandbox)
   void              SaveParam_passed(SaveParam_type type);// Eligiendo los datos para registrar en el archivo
   void              OptimisationParam_selected();// Eligiendo el parámetro de optimización y rellenando la pestaña "Optimisation selection"
   void              CompareTables(bool isChecked);// Construyendo la distribución según el recuadro con los resultados (para que se corresponda con el recuadro general(principal))
   void              show_FriquencyChart(bool isChecked);// Mostrando el gráfico de frecuencias de beneficio/pérdidas
   void              FriquencyChart_click();// Eligiendo la línea en el recuadro con los coeficientes y construyendo la distribución
   void              Filtre_click();// Filtrando según las condiciones elegidas
   void              PL_pressed_3(bool isRealPL);// Construyendo los gráficos de beneficio/pérdidas según todos los datos del recuadro Result
   void              PL_pressed_4(bool isRealPL);// Construyendo los recuadros con las estadísticas
   void              DaySelect();// Eligiendo el Día del recuadro de PL por días
   void              Reset_click();// Reseteando filtros
   void              setChartFlag(bool isPlot);// Condición para construir (o no construir) el gráfico del método PL_pressed_3(bool isRealPL);

private:
   CWindowManager   *windowManager;// Enlace a la clase que representa la ventana
   CDBReader         dbReader;// Clase que trabaja con la base de datos
   CReportCreator    reportCreator; // Clase que procesa los datos

   CGenericSorter    sorter; // Clase de clasificación
   CoefData_comparer coefComparer; // Clase que compara los datos

   void              loadData();// Descargando los datos de la base y rellenando los recuadros

   void              insertDataTo_main_Table(bool isResult,const CoefData_item &data[]); // Pega los datos en el recuadro con los resultados, y también en el recuadro "Principal" (recuadros con los coeficientes de las pasadas de optimización)
   void              insertRowTo_main_Table(CTable *tb,int n,const CoefData_item &data); // Pegando propiamente los datos en el recuadro con las pasadas de optimización
   void              selectChartByID(long ID,bool recalc=true);// Eligiendo los gráficos según la ID
   void              createReport();// Creando informe
   string            getCorrectPath(string path,string name);// Obteniendo la ruta correcta al archivo
   bool              getPLChart(PLChart_item &data[],bool isOneLot,long ID);

   bool              curveAdd(CGraphic *chart_ptr,const PLChart_item &data[],bool isHist);// Añadiendo el gráfico a Other Charts
   bool              curveAdd(CGraphic *chart_ptr,const CoefChart_item &data[],double borderPoint);// Añadiendo el gráfico a Other Charts
   bool              curveAdd(CGraphic *chart_ptr,const Distribution_item &data);// Añadiendo el gráfico a Other Charts
   void              setCombobox(CComboBox *cb_ptr,CArrayString &arr,bool isFirstIndex=true);// Estableciendo los parámetros del cuadro combinado
   void              addPDF_line(CGraphic *chart_ptr,double &x[],color clr,int width,string _name=NULL);// Añadiendo la línea suavizada del gráfico de distribución
   void              plotMainPDF();// Construyendo la distribución según el recuadro "Principal" (Optimisation Data)
   void              updateDT(CDropCalendar *dt_ptr,datetime DT);// Actualizando los calendarios desplegables

   CParamsFiltre     coefKeeper;// Clasificador de pasadas de optimización (clasificamos por distribuciones)
   CArrayString      headder; // Encabezado de los recuadros con los coeficientes

   bool              _isUpbateClick; // Propiedad de pulsación del botón "Update" - y descarga de la base de datos.
   long              _selectedID; // ID de la serie seleccionada del gráfico de todos los PL (rojo si tiene pérdidas, o verde si tiene beneficios)
   long              _ID,_ID_Arr[];// Matriz de la IDs en el recuadro Result después de descargar los datos
   bool              _IsForvard_inTables,_IsForvard_inReport; // bandera del tipo de datos de la optimización en los recuadros de las pasadas de optimización
   datetime          _DT_from,_DT_till;
   double            _Gap; // Tipo guardado del gap añadido (imitación de expansión del spread/o deslizamiento...) del anterior gráfico de optimización elegido
  };

Cada una de las llamadas de retorno ha sido descrita y comentada con suficiente detalle, así que no hay necesidad de repetirlo. Solo tenemos que decir que se trata precisamente de aquella parte de la aplicación donde se implementa todo el comportamiento del formulario. Aquí se implementa la construcción de los gráficos, el rellenado de los cuadros combinados, la llamada de los métodos de carga de datos desde la base y su procesamiento, y otras operaciones que conectan varias clases.


Conclusión

Como resultado, hemos conseguido una aplicación que procesa un recuadro con todas las pasadas posibles a través del simulador de parámetros de optimización, además de una adición al robot que guarda todas las pasadas de optimizaición de la base de datos. Aparte del informe de transacciones detallado que obtenemos al elegir el parámetro que nos interesa, gracias a este programa podemos ver con detalle cualquier intervalo que nos interese de toda la historia de optimización, seleccionándolo según el tiempo, así como todos los coeficientes en el intervalo de tiempo dado. Asimismo, podemos simular el deslizamiento, aumentando el parámetro Gap, y ver cómo cambia en función de ello el comportamiento de los gráficos y coeficientes. Otro complemento es la posibilidad de filtrar los resultados de optimización en un intervalo determinado de valores de los coeficientes.

El método más sencillo de conseguir las 100 mejores pasadas es conectar la clase CDBWriter al robot de usted como en el robot del ejemplo (se encuentra en los archivos adjuntos), establecer el filtro condicional (por ejemplo Factor de Beneficio >= 1 y esto descartará de una vez todas las combinaciones con pérdidas) y pulsar el botón Update, dejando en este caso un parámetro igual a 100 en los ajustes "Show n params". Entonces, en el recuadro con los resultados se filtrarán las 100 mejores pasadas de optimización (según el filtro que usted haya establecido). En el siguiente artículo se analizará con detalle cada una de las opciones de la aplicación obtenida y también una selección más detallada de los coeficientes.


Al artículo se adjuntan los siguientes archivos:

Experts/2MA_Martin — proyecto del robot de prueba

  • 2MA_Martin.mq5 — archivo con el código de la plantilla del robot. En este se incluye el archivo DBWriter.mqh, que guarda los datos de optimización en la base de datos
  • Robot.mq5 — archivo con la implementación de la lógica del robot
  • Robot.mqh — Archivo de encabezado que se implementa en el archivo Robot.mq5
  • Trade.mq5 — archivo con la implementación de la lógica comercial del robot
  • Trade.mqh — Archivo de encabezado que se implementa en el archivo Trade.mq5

Experts/OptimisationSelector — proyecto de la aplicación descrita

  • OptimisationSelector.mq5 — archivo con la plantilla del robot donde se llama todo el código del proyecto
  • ParamsFiltre.mq5 — archivo con la implementación del filtro y la construcción de las distribuciones según los recuadros con los resultados
  • ParamsFiltre.mqh — Archivo de encabezado que se implementa en el archivo ParamsFiltre.mq5
  • Presenter.mq5 — archivo con la implementación del presenter
  • Presenter.mqh — Archivo de encabezado que se implementa en el archivo Presenter.mq5
  • Presenter_interface.mqh — Archivo de la interfaz del presenter
  • Window_1.mq5 — Archivo con la implementación de la construcción del gráfico
  • Window_1.mqh — Archivo de encabezado que se implementa en el archivo Window_1.mq5

Include/CustomGeneric

  • GenericSorter.mqh — archivo con la implementación de la clasificación de los datos
  • ICustomComparer.mqh — Interfaz ICustomSorter

Include/History manager

  • DealHistoryGetter.mqh — Archivo con la implementación de la descarga de la historia de transacciones desde el terminal y su conversión al aspecto necesario
  • ReportCreator.mqh — Archivo con la implementación de la clase que crea la historia de transacciones

Include/OptimisationSelector

  • DataKeeper.mqh — Archivo con la implementación de la clase para almacenar los coeficientes del robot asociado con el nombre del coeficiente
  • DBReader.mqh — Archivo con la implementación de la clase que lee desde la base de datos los recuadros necesarios
  • DBWriter.mqh — Archivo con la implementación de la clase que escribe en la base de datos

Include/Sqlite3

  • sqlite_amalgmation.mqh — Archivo con la importación de las funciones de trabajo con la base
  • SqliteManager.mqh — Archivo con la implementación del conector a la base de datos y a la clase de la instrucción
  • SqliteReader.mqh — Archivo con la implementación de la clase que lee las respuestas desde la base de datos

Include/WinApi

  • memcpy.mqh — Importa la función memcpy
  • Mutex.mqh — Importa las funciones de creación de Mutex
  • strcpy.mqh — Importa la función strcpy
  • strlen.mqh — Importa la función strlen

Libraries

  • Sqlite3_32.dll — Dll Sqlite para los terminales de 32 bits
  • Sqlite3_64.dll — Dll Sqlite para los terminales de 64 bits

Base de datos de prueba

  • 2MA_Martin optimisation data - Base de datos Sqlite

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

Archivos adjuntos |
MQL5.zip (6783.27 KB)
WebRequest multiflujo asincrónico en MQL5 WebRequest multiflujo asincrónico en MQL5
En el artículo se analiza una biblioteca que permite aumentar la efectividad del trabajo con solicitudes HTTP en MQL5. La ejecución de WebRequest en el modo no bloqueante se ha implementado en flujos adicionales usando gráficos y expertos auxiliares, intercambio de eventos personalizados y lectura de recursos compartidos. Se adjuntan los códigos fuente.
Reversión: disminuyendo la reducción máxima y simulando otros mercados Reversión: disminuyendo la reducción máxima y simulando otros mercados
En este artículo continuaremos analizando el tema de la reversión. Intentaremos disminuir la reducción máxima del balance hasta un nivel aceptable con los instrumentos analizados anteriormente. También vamos a comprobar si se reduce el beneficio obtenido. Asimismo, comprobaremos cómo funciona la reversión en otros mercados, tales como los mercados de valores, materias primas, índices y ETF, agrario. ¡Atención, el artículo contiene muchas imágenes!
Aplicación de OpenCL para simular patrones de velas Aplicación de OpenCL para simular patrones de velas
En este artículo analizaremos el algoritmo de implementación de un simulador de patrones de velas en el lenguaje OpenCL en el modo "OHLC en M1". Asimismo, compararemos su rapidez con el simulador de estrategias incorporado en el modo de optimización rápida y lenta.
Patrones de viraje: Poniendo a prueba el patrón "Pico/valle doble" Patrones de viraje: Poniendo a prueba el patrón "Pico/valle doble"
En la práctica del comercio, los tráders buscan con frecuencia los puntos de viraje de tendencia, puesto que precisamente en el momento en el que surge una tendencia, el precio tiene el mayor potencial de movimiento. Precisamente por ello, en la práctica del análisis técnico se analizan diferentes patrones de viraje. Uno de los más famosos y más utilizados en el patrón del pico/valle doble. En este artículo ofrecemos una variante de detección automática del patrón, y también ponemos a prueba su rentabilidad con datos históricos.