Las 100 mejores pasadas de optimización (Parte 1). Creando un analizador de optimizaciones
- Introducción
- Estructura del analizador de optimizaciones
- Gráficos
- Trabajando con la base de datos
- Cálculos
- El "Presenter"
- Conclusión
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; в метод 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: |
---|
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:
- Adición del puntero al elemento principal: es necesario que el contenedor de pestañas sepa a qué elemento está fijado
- Línea de creación del elemento de control
- 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:
- Time — eje X (rótulo de intervalo temporal)
- PL_total — beneficio/pérdidas, si incrimentamos el lote en proporción al robot
- PL_oneLot — beneficio/pérdidas, si comerciamos todo el tiempo con un lote
- DD_total — reducción, si comerciamos con el lote de la misma forma que ha comerciado el robot
- DD_oneLot — reducción, si comerciamos con un lote
- isForvard — propiedad del gráfico en tiempo real
Recuadro OptimisationParams:
- ID — Número único de rellenado automático de entrada en la base de datos
- HistoryBorder — Fecha de finalización de la optimización histórica
- TF — Marco temporal
- Param_1...Param_n — parámetro
- InitalBalance — valor del balance inicial
Recuadro ParamsCoefitients:
- ID — Clave externa, enlace a OptimisationParams(ID)
- isForvard — propiedad de optimización en tiempo real
- isOneLot — propiedad del gráfico según el cual se ha calculado el coeficiente
- DD — reducción
- averagePL — beneficio/pérdidas medios según el gráfico de PL
- averageDD — reducción media
- averageProfit — beneficio medio
- profitFactor — factor de beneficio
- recoveryFactor — factor de recuperación
- sharpRatio — ratio de Sharpe
- altman_Z_Score — Puntuación Z de Altman
- VaR_absolute_90 — VaR 90
- VaR_absolute_95 — VaR 95
- VaR_absolute_99 — VaR 99
- VaR_growth_90 — VaR 90
- VaR_growth_95 — VaR 95
- VaR_growth_99 — VaR 99
- winCoef — coeficiente de ganancia
- customCoef — coeficiente personalizado
Recuadro ParamType:
- ParamName — nombre del parámetro del robot
- ParamType — tipo del parámetro del robot (int / double / string)
Recuadro TradingHistory
- ID — Clave externa del enlace a OptimisationParams(ID)
- isForvard — propiedad de la historia de simulación en tiempo real
- Symbol — símbolo
- DT_open — fecha de apertura
- Day_open — día de apertura
- DT_close — fecha de cierre
- Day_close — día de cierre
- Volume — Número de lotes
- isLong — propiedad de long o short
- Price_in — precio de entrada
- Price_out — precio de salida
- PL_oneLot — Beneficio al comerciar con un lote
- PL_forDeal — Beneficio al comerciar como se ha hecho anteriormente
- OpenComment — comentario de entrada
- 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:
- El nombre de la variable del robot
- El tipo de la variable
- 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.
- 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:
- o bien el usuario ha detenido por algún motivo la optimización histórica y la ha iniciado de nuevo,
- 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 ¶ms[],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
- 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
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso