Desarrollamos un asesor experto multidivisas (Parte 25): Conectamos una nueva estrategia (II)
Introducción
Continuemos con la siguiente fase del trabajo iniciado en el artículo anterior. Permítame recordarle que después de dividir todo el código del proyecto en las partes de la biblioteca y el proyecto, decidimos comprobar cómo podíamos cambiar de la estrategia comercial del modelo SimpleVolumes utilizada durante mucho tiempo a alguna otra. ¿Qué tendremos que hacer para conseguirlo? ¿Cómo de fácil será? Fue necesario escribir una clase de nueva estrategia comercial. Pero surgieron más complicaciones cuya aparición no era tan evidente.
Estaban relacionadas precisamente con el deseo de hacer posible que la parte de biblioteca fuera independiente de la parte de diseño. Si hubiéramos decidido incumplir esta novedosa norma, tampoco habrían surgido dificultades. Sin embargo, al final encontramos la forma de mantener el código separado y garantizar la conexión de la nueva estrategia comercial. Esto requirió cambios en los archivos de la parte de biblioteca del proyecto, no muy grandes en tamaño, pero sustanciales en cuanto a su significado.
Al final, pudimos compilar y ejecutar la primera fase de optimización del EA con una nueva estrategia que denominamos SimpleCandles. Los siguientes pasos consistieron en hacer que el transportador de optimización automática funcionara con él. Para la última estrategia desarrollamos el asesor experto auxiliar CreateProject.mq5, que nos permitía generar tareas en la base de datos de optimización para su ejecución en el transportador. En los parámetros del asesor experto podemos especificar en qué instrumentos comerciales (símbolos) y marcos temporales queremos optimizar, cuáles son los nombres de los asesores expertos de las etapas y otra información necesaria. Si la base de datos de optimización no existía antes, se creaba automáticamente.
Veamos ahora cómo hacer funcionar esto con la nueva estrategia comercial.
Trazando el camino
Empezaremos el trabajo principal analizando el código del asesor experto CreateProject.mq5. Nuestro objetivo será identificar el código que sea igual, o casi igual, en los distintos proyectos. Este código puede separarse en una parte de biblioteca, dividiéndolo en varios archivos independientes, de ser necesario. La parte del código que será diferente para los distintos proyectos, la dejaremos en la parte del proyecto y describiremos qué cambios habrá que hacerle.
Pero antes, solucionaremos el error detectado que se produce al guardar la información sobre las pasadas del probador en la base de datos de optimización, a perfeccionar las macros para organizar los ciclos y a ver cómo podemos añadir parámetros adicionales a la estrategia comercial desarrollada anteriormente.
Correcciones en la base de datos CD
En los últimos artículos, hemos empezado a usar intervalos de prueba relativamente pequeños para los proyectos de optimización. En lugar de intervalos de 5 o más años, hemos empezado a tomar intervalos de unos pocos meses. Esto se debe al hecho de que nuestra tarea principal consistía en comprobar el funcionamiento del mecanismo de transportador de optimización automática , y acortar el intervalo nos permitía reducir el tiempo de una pasada individual del probador y, por tanto, el tiempo total de optimización.
Para guardar la información de las pasadas en la base de datos de optimización, cada agente de prueba (local, remoto o en la nube) la envía como parte de un marco de datos al terminal en el que se está ejecutando el proceso de optimización. En este terminal, tras el inicio de la optimización, se inicia una instancia adicional del asesor experto optimizado en un modo especial: el modo de recopilación de marcos de datos. Esta instancia no se ejecuta en el probador, sino en un gráfico de terminal independiente. Es la que recibirá y realizará el guardado de toda la información procedente de los agentes de prueba.
Aunque el código del manejador de eventos para la llegada de nuevos marcos de datos de los agentes de prueba no contiene operaciones asíncronas, durante la optimización empezaron a aparecer mensajes sobre errores de inserción en la base de datos relacionados con el bloqueo de la base de datos debido a otra operación. Este error era relativamente infrecuente. Sin embargo, algunas decenas de pasadas entre varios miles acabaron por no añadir sus resultados a la base de datos de optimización.
Parece que la razón de estos errores es el aumento del número de situaciones en las que varios agentes de prueba finalizan simultáneamente otra pasada y envían un marco de datos al asesor experto en el terminal principal. Y este EA intenta insertar un nuevo registro en la base de datos más rápido que el procesamiento de la operación de inserción anterior en el lado de la base de datos.
Para solucionar esto, añadiremos un manejador independiente para esta categoría de errores. Si la causa del error es el bloqueo de la base de datos o de la tabla a causa de otra operación, bastará con repetir la operación fallida al cabo de un tiempo. Si después de un cierto número de intentos de reinsertar los datos, se produce nuevamente el mismo error, entonces deberemos dejar de intentarlo.
Para la inserción usaremos el método CDatabase::ExecuteTransaction(), por lo que realizaremos los siguientes cambios en él. Añadiremos a los argumentos del método un contador de intentos de ejecución de la solicitud. Si se produce un error de este tipo, haremos una pausa de un número aleatorio de milisegundos (0 - 50) y llamaremos a la misma función con el valor incrementado del contador de intentos.
//+------------------------------------------------------------------+ //| Execute multiple DB queries in one transaction | //+------------------------------------------------------------------+ bool CDatabase::ExecuteTransaction(string &queries[], int attempt = 0) { // Open a transaction DatabaseTransactionBegin(s_db); s_res = true; // Send all execution requests FOREACH(queries, { s_res &= DatabaseExecute(s_db, queries[i]); if(!s_res) break; }); // If an error occurred in any request, then if(!s_res) { // Cancel transaction DatabaseTransactionRollback(s_db); if((_LastError == ERR_DATABASE_LOCKED || _LastError == ERR_DATABASE_BUSY) && attempt < 20) { PrintFormat(__FUNCTION__" | ERROR: ERR_DATABASE_LOCKED. Repeat Transaction in DB [%s]", s_fileName); Sleep(rand() % 50); ExecuteTransaction(queries, attempt + 1); } else { // Report it PrintFormat(__FUNCTION__" | ERROR: Transaction failed in DB [%s], error code=%d", s_fileName, _LastError); } } else { // Otherwise, confirm transaction DatabaseTransactionCommit(s_db); //PrintFormat(__FUNCTION__" | Transaction done successfully"); } return s_res; }
Por si acaso, realizaremos los mismos cambios en cuanto a nivel de significación en el método de ejecución de la consulta SQL sin transacción CDatabase::Execute().
Otro pequeño cambio que podríamos usar en el futuro es la adición de una variable booleana estática a la clase CDatabase. En ella se recordará que se ha producido un error al ejecutar las consultas:
//+------------------------------------------------------------------+ //| Class for handling the database | //+------------------------------------------------------------------+ class CDatabase { // ... static bool s_res; // Query execution result public: static int Id(); // Database connection handle static bool Res(); // Query execution result // ... }; bool CDatabase::s_res = true;
Los cambios realizados los guardaremos en el archivo Database/Database.mqh de la carpeta de la biblioteca.
Correcciones en Macros.h
Asimismo, mencionaremos un cambio que debería haberse producido hace tiempo, pero al que nunca le había llegado su turno. Recuerde que para simplificar la escritura de los encabezados de ciclo que deben iterar todos los valores de un array, creamos la macro FOREACH(A, D):
#define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
Aquí A es el nombre del array, mientras que D es el cuerpo del ciclo. Esta implementación tenía la desventaja de que durante la depuración normalmente no se podía seguir la ejecución del código paso a paso dentro del cuerpo del ciclo. Y aunque rara vez era necesario, resultaba muy incómodo. Una vez, mientras ojeando la documentación, vi otra forma de implementar una macro similar. Allí, la macro solo especificaba el encabezado del ciclo, mientras que el cuerpo lo sacamos fuera de la macro. Sin embargo, había un parámetro más que establecía el nombre de la variable de ciclo.
En nuestra implementación anterior, el nombre de la variable del ciclo (índice del elemento del array) era fijo (i), y esto no causaba problemas en ninguna parte. Incluso en el lugar donde se necesitaba un ciclo doble, podíamos prescindir de los mismos nombres debido a los diferentes ámbitos de estos índices. Por ello, la nueva aplicación también tiene un nombre de índice fijo. El único parámetro que se transmite es el nombre del array sobre el que se realiza el ciclo:
#define FOREACH(A) for(int i=0, im=ArraySize(A);i<im;i++)
Así, tuvimos que editar todos los lugares donde se utilizaba esta macro para cambiar a la nueva versión. Por ejemplo:
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CAdvisor::Tick(void) { // Call OnTick handling for all strategies //FOREACH(m_strategies, m_strategies[i].Tick();) FOREACH(m_strategies) m_strategies[i].Tick(); }
Junto con esta macro, hemos añadido otra macro para crear el encabezado del ciclo. En ella, cada elemento del array A se coloca a su vez en una variable llamada E, que debe declararse previamente. Antes del encabezado del ciclo, el primer elemento del array, si existe, se coloca en esta variable. Como variable de ciclo, usaremos una variable con un nombre formado por la letra i y el nombre de la variable E. En la tercera parte de el encabezado del ciclo, incrementaremos la variable del ciclo y asignaremos el valor del elemento del array A con el índice incrementado a la variable E. El uso de la operación de índice con el módulo del número de elementos del array permite evitar exceder los límites del array en la última iteración del ciclo.
#define FOREACH_AS(A, E) if(ArraySize(A)) E=A[0]; \ for(int i##E=0, im=ArraySize(A);i##E<im;E=A[++i##E%im])
Vamos a guardar los cambios realizados en el archivo Utils/Macros.h en la carpeta de la biblioteca.
Cómo añadir un parámetro a una estrategia comercial
Como casi todo código de software, la implementación de una estrategia comercial también está sujeta a cambios. Si estos cambios están relacionados con la modificación de la composición de los parámetros de entrada de una única instancia de una estrategia comercial, entonces deberemos realizar cambios no solo en la clase de estrategia comercial, sino también en algunos otros lugares. Veamos un ejemplo.
Supongamos que hemos decidido añadir el parámetro de spread máximo a la estrategia comercial. Su uso consistirá en que si en el momento de recibir una señal para abrir una posición el spread actual supera el valor establecido en este parámetro, la posición no se abrirá.
En primer lugar, en la primera etapa EA añadiremos un parámetro de entrada a través del cual podremos establecer este valor cuando se ejecute en el probador. A continuación, en la función de formación de la cadena de inicialización, añadiremos la sustitución del valor del parámetro añadido en la cadena de inicialización:
//+------------------------------------------------------------------+ //| 4. Strategy inputs | //+------------------------------------------------------------------+ sinput string symbol_ = ""; // Symbol sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; // Timeframe for candles input group "=== Opening signal parameters" input int signalSeqLen_ = 6; // Number of unidirectional candles input int periodATR_ = 0; // ATR period (if 0, then TP/SL in points) input group "=== Pending order parameters" input double stopLevel_ = 25000; // Stop Loss (in ATR fraction or points) input double takeLevel_ = 3630; // Take Profit (in ATR fraction or points) input group "=== Money management parameters" input int maxCountOfOrders_ = 9; // Max number of simultaneously open orders input int maxSpread_ = 10; // Max acceptable spread (in points) //+------------------------------------------------------------------+ //| 5. Strategy initialization string generation function | //| from the inputs | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); }
La cadena de inicialización contendrá ahora un parámetro más que antes. Por lo tanto, el siguiente cambio consistiría en añadir una nueva propiedad de clase y lectura en ella del valor de la cadena de inicialización en el constructor:
//+------------------------------------------------------------------+ //| Trading strategy using unidirectional candlesticks | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: // ... //--- Money management parameters int m_maxCountOfOrders; // Max number of simultaneously open positions int m_maxSpread; // Max acceptable spread (in points) // ... }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) { // Read the parameters from the initialization string m_params = p_params; m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params); m_signalSeqLen = (int) ReadLong(p_params); m_periodATR = (int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_maxCountOfOrders = (int) ReadLong(p_params); m_maxSpread = (int) ReadLong(p_params); // ... }
Eso es todo, ahora podremos utilizar el nuevo parámetro como queramos en los métodos de la clase de estrategia comercial. Según su finalidad, podremos añadir el siguiente código al método de recepción de una señal para abrir una posición.
//+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int CSimpleCandlesStrategy::SignalForOpen() { // By default, there is no signal int signal = 0; MqlRates rates[]; // Copy the quote values (candles) to the destination array. // To check the signal we need m_signalSeqLen of closed candles and the current candle, // so in total m_signalSeqLen + 1 int res = CopyRates(m_symbol, m_timeframe, 0, m_signalSeqLen + 1, rates); // If the required number of candles has been copied if(res == m_signalSeqLen + 1) { signal = 1; // buy signal // Go through all closed candles for(int i = 1; i <= m_signalSeqLen; i++) { // If at least one upward candle occurs, cancel the signal if(rates[i].open < rates[i].close ) { signal = 0; break; } } if(signal == 0) { signal = -1; // otherwise, sell signal // Go through all closed candles for(int i = 1; i <= m_signalSeqLen; i++) { // If at least one downward candle occurs, cancel the signal if(rates[i].open > rates[i].close ) { signal = 0; break; } } } } // If there is a signal, then if(signal != 0) { // If the current spread is greater than the maximum allowed, then if(rates[0].spread > m_maxSpread) { PrintFormat(__FUNCTION__" | IGNORE %s Signal, spread is too big (%d > %d)", (signal > 0 ? "BUY" : "SELL"), rates[0].spread, m_maxSpread); signal = 0; // Cancel the signal } } return signal; }
Del mismo modo, podemos añadir otros parámetros nuevos a las estrategias comerciales o deshacernos de parámetros que se hayan vuelto innecesarios.
Análisis de CreateProject.mq5
Empezaremos analizando el código del asesor de creación de proyectos CreateProject.mq5. En su función de inicialización, ya hemos dividido el código en funciones separadas. La finalidad de cada una de ellas se desprende claramente de su nombre:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Connect to the database DB::Connect(fileName_); // Create a project CreateProject(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE) ) ); // Create project stages CreateStages(); // Creating jobs and tasks CreateJobs(); // Queueing the project for execution QueueProject(); // Close the database DB::Close(); // Successful initialization return(INIT_SUCCEEDED); }
Pero esta división no es muy cómoda, porque las funciones asignadas han resultado bastante engorrosas y resuelven tareas bastante heterogéneas. Por ejemplo, en la función CreateJobs(), nos ocupamos de preprocesar los datos de entrada y formar plantillas de parámetros para los trabajos, así como de insertar la información en la base de datos, y luego también realizamos acciones similares para crear tareas de optimización en la base de datos. Y lo deseable sería que se diera al contrario: las funciones serían más sencillas y resolverían una tarea pequeña.
Para usar la nueva estrategia en la aplicación actual, tendríamos que cambiar la plantilla de parámetros de la primera etapa, y posiblemente también el número de tareas con criterios de optimización para ella. La plantilla de los parámetros de la primera etapa para la estrategia comercial pasada se establece en la variable global paramsTemplate1:
// Template of optimization parameters at the first stage string paramsTemplate1 = "; === Open signal parameters\n" "signalPeriod_=212||12||40||240||Y\n" "signalDeviation_=0.1||0.1||0.1||2.0||Y\n" "signaAddlDeviation_=0.8||0.1||0.1||2.0||Y\n" "; === Pending order parameters\n" "openDistance_=10||0||10||250||Y\n" "stopLevel_=16000||200.0||200.0||20000.0||Y\n" "takeLevel_=240||100||10||2000.0||Y\n" "ordersExpiration_=22000||1000||1000||60000||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=3||3||1||30||N\n";
Afortunadamente, nos ocurrió lo mismo con todo el trabajo de optimización de la primera fase. Pero no siempre es así. Por ejemplo, en la nueva estrategia, hemos incluido en los parámetros los valores del símbolo y el marco temporal en el que debe funcionar la estrategia. Esto significa que los distintos trabajos de optimización de la primera fase creados para distintos símbolos y marcos temporales tendrán partes variables en la plantilla de parámetros. No obstante, para establecer sus valores, tendremos que entrar en el código de la función de creación de trabajo y realizar cambios en él. Entonces no podremos mover a la sección de la biblioteca.
Además, nuestro asesor de creación de proyectos de optimización creará ahora un proyecto con tres fases fijas. Hemos llegado a este sencillo conjunto de pasos durante el proceso de desarrollo, aunque hemos probado a añadir pasos adicionales (véanse, por ejemplo, la parte 18 y la parte 19). Los pasos adicionales no han mostrado mejoras significativas en el resultado final, aunque este puede no ser el caso de otras estrategias comerciales. Por consiguiente, si ponemos el código actual en la parte de la biblioteca, no podremos cambiar la composición de las etapas en el futuro, si lo deseáramos.
Así que, por mucho que queramos hacer pequeños esfuerzos, resulta mejor trabajar en serio en la refactorización de este código ahora que posponerlo para más adelante. Vamos a intentar dividir el código del asesor de creación de proyectos en varias clases. Las clases se llevarán a la parte de la biblioteca, y en la parte del proyecto las usaremos para crear proyectos con la composición deseada de etapas y su relleno. Al mismo tiempo, esto también supondrá una preparación preliminar para el futuro para mostrar información sobre el progreso del funcionamiento del transportador.
Para empezar, vamos a intentar escribir cómo sería el código final. Esta versión preliminar ha permanecido prácticamente inalterada hasta llegar a su versión de trabajo. Solo hemos añadido composiciones de parámetros específicos a las llamadas a los métodos. Veamos cómo es la nueva versión de la función de inicialización del asesor experto de creación de proyectos de optimización. Para no distraernos con detalles menores, no mostraremos los argumentos de los métodos:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Create an optimization project object for the given database COptimizationProject p; // Create a new project in the database p.Create(...); // Add the first stage p.AddStage(...); // Adding the first stage jobs p.AddJobs(...); // Add tasks for the first stage jobs p.AddTasks(...); // Add the second stage p.AddStage(...); // Add the second stage jobs p.AddJobs(...); // Add tasks for the second stage jobs p.AddTasks(...); // Add the third stage p.AddStage(...); // Add the third stage job p.AddJobs(...); // Add a task for the third stage job p.AddTasks(...); // Put the project in the execution queue p.Queue(); // Delete the EA ExpertRemove(); // Successful initialization return(INIT_SUCCEEDED); }
Con esta estructura de código, podemos añadir fácilmente nuevas etapas y cambiar sus parámetros con flexibilidad. Pero hasta ahora, solo vemos una nueva clase que sin duda necesitaremos: la clase de proyecto de optimización COptimisationProject. Echemos un vistazo a su código.
Clase COptimisationProject
Al desarrollar esta clase, quedó claro rápidamente que necesitaríamos clases aparte para todos los tipos de entidades que almacenemos en la base de datos de optimización. Es decir, a continuación vendrán las clases COptimisationStage para las etapas del proyecto, las clases COptimisationJob para el funcionamiento de las etapas del proyecto y las clases COptimisationTask para las tareas de cada trabajo de las etapas del proyecto.
Como los objetos de estas clases son, de hecho, representaciones de registros de diferentes tablas de la base de datos de optimización, la composición de los campos de la clase repetirá la composición de los campos de las tablas correspondientes. Además de estos campos, añadiremos otros campos y métodos a estas clases según resulte necesario para realizar las tareas asignadas.
Por ahora, para simplificar, haremos públicas todas las propiedades y métodos de las clases creadas. Cada clase tendrá su propio método para crear un nuevo registro en la base de datos de optimización. Más adelante añadiremos métodos para la modificación de un registro existente y la lectura de un registro desde la base de datos, ya que no lo necesitaremos al crear el proyecto.
En lugar de las plantillas de parámetros del comprobador utilizadas anteriormente, crearemos funciones independientes que retornarán parámetros ya rellenados de acuerdo con la plantilla. Así, las plantillas de parámetros se moverán dentro de estas funciones. Estas funciones tomarán como parámetro el puntero al proyecto y podrán acceder a la información necesaria del proyecto para sustituirla en la plantilla a través de él. Declararemos estas funciones en la parte del proyecto, mientras que en la parte de la biblioteca declararemos solo un nuevo tipo, un puntero a la función del siguiente tipo:
// Create a new type - a pointer to a string generation function // for optimization job parameters (job) accepting the pointer // to the optimization project object as an argument typedef string (*TJobsTemplateFunc)(COptimizationProject*);
Gracias a esto, podremos usar funciones de la clase COptimisationProject para generar los parámetros de etapa que aún no tenemos, pero que en el futuro, en la parte del proyecto, tendremos que añadir sin duda.
Aquí tenemos la descripción de esta clase:
//+------------------------------------------------------------------+ //| Optimization project class | //+------------------------------------------------------------------+ class COptimizationProject { public: string m_fileName; // Database name // Properties stored directly in the database ulong id_project; // Project ID string name; // Name string version; // Version string description; // Description string status; // Status // Arrays of all stages, jobs and tasks COptimizationStage* m_stages[]; // Project stages COptimizationJob* m_jobs[]; // Jobs of all project stages COptimizationTask* m_tasks[]; // Tasks of all jobs of project stages // Properties for the current state of the project creation string m_symbol; // Current symbol string m_timeframe; // Current timeframe COptimizationStage* m_stage; // Last created stage (current stage) COptimizationJob* m_job; // Last created job (current job) COptimizationTask* m_task; // Last created task (current task) // Methods COptimizationProject(string p_fileName); // Constructor ~COptimizationProject(); // Destructor // Create a new project in the database COptimizationProject* COptimizationProject::Create(string p_name, string p_version = "", string p_description = "", string p_status = "Done"); void Insert(); // Insert an entry into the database void Update(); // Update an entry in the database // Add a new stage to the database COptimizationProject* AddStage(COptimizationStage* parentStage, string stageName, string stageExpertName, string stageSymbol, string stageTimeframe, int stageOptimization, int stageModel, datetime stageFromDate, datetime stageToDate, int stageForwardMode, datetime stageForwardDate, int stageDeposit = 10000, string stageCurrency = "USD", int stageProfitInPips = 0, int stageLeverage = 200, int stageExecutionMode = 0, int stageOptimizationCriterion = 7, string stageStatus = "Done"); // Add new jobs to the database for the specified symbols and timeframes COptimizationProject* AddJobs(string p_symbols, string p_timeframes, TJobsTemplateFunc p_templateFunc); COptimizationProject* AddJobs(string &p_symbols[], string &p_timeframes[], TJobsTemplateFunc p_templateFunc); // Add new tasks to the database for the specified optimization criteria COptimizationProject* AddTasks(string p_criterions); COptimizationProject* AddTasks(string &p_criterions[]); void Queue(); // Put the project in the execution queue // Convert a string name to a timeframe static ENUM_TIMEFRAMES StringToTimeframe(string s); };
Primero vienen las propiedades, que se almacenan directamente en la base de datos de optimización, en la tabla projects. A continuación vienen los arrays de todas las fases, actividades y tareas del proyecto, seguidos de las propiedades del estado actual del proceso de creación del proyecto.
Como esta clase ahora solo tiene una tarea (crear un proyecto en la base de datos de optimización), en el constructor nos conectaremos inmediatamente a la base de datos requerida y abriremos una transacción. La finalización de esta operación tendrá lugar en el destructor. Aquí es donde resultará útil el campo estático de la clase CDatabase::s_res, por cuyo valor podremos entender si se ha producido algún error durante las operaciones de inserción de registros en la base de datos de optimización al crear el proyecto. Si no hay errores, la transacción se confirmará; en caso contrario, se cancelará. El destructor también liberará memoria para los objetos dinámicos creados.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ COptimizationProject::COptimizationProject(string p_fileName) : m_fileName(p_fileName), id_project(0) { // Connect to the database if (DB::Connect(m_fileName)) { // Start a transaction DatabaseTransactionBegin(DB::Id()); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ COptimizationProject::~COptimizationProject() { // If no errors occurred, then if(DB::Res()) { // Confirm the transaction DatabaseTransactionCommit(DB::Id()); } else { // Otherwise, cancel the transaction DatabaseTransactionRollback(DB::Id()); } // Close connection to the database DB::Close(); // Delete created task, job, and stage objects FOREACH(m_tasks) { delete m_tasks[i]; } FOREACH(m_jobs) { delete m_jobs[i]; } FOREACH(m_stages) { delete m_stages[i]; } }
Los métodos para añadir actividades y tareas se declararán en dos variantes. En la primera variante, las listas de símbolos, marcos temporales y criterios se transmitirán en parámetros de cadena separados por comas. Dentro del método, estas cadenas se convertirán en arrays de valores y se sustituirán como argumentos al llamar a la segunda variante del método, que solo acepta arrays.
Estos son los métodos para añadir tareas:
//+------------------------------------------------------------------+ //| Add new jobs to the database for the specified | //| symbols and timeframes in strings | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(string p_symbols, string p_timeframes, TJobsTemplateFunc p_templateFunc) { // Array of symbols for strategies string symbols[]; StringReplace(p_symbols, ";", ","); StringSplit(p_symbols, ',', symbols); // Array of timeframes for strategies string timeframes[]; StringReplace(p_timeframes, ";", ","); StringSplit(p_timeframes, ',', timeframes); return AddJobs(symbols, timeframes, p_templateFunc); } //+------------------------------------------------------------------+ //| Add new jobs to the database for the specified | //| symbols and timeframes in arrays | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(string &p_symbols[], string &p_timeframes[], TJobsTemplateFunc p_templateFunc) { // For each symbol FOREACH_AS(p_symbols, m_symbol) { // For each timeframe FOREACH_AS(p_timeframes, m_timeframe) { // Get the parameters for work for a given symbol and timeframe string params = p_templateFunc(&this); // Create a new job object m_job = new COptimizationJob(0, m_stage, m_symbol, m_timeframe, params); // Insert it into the optimization database m_job.Insert(); // Add it to the array of all jobs APPEND(m_jobs, m_job); // Add it to the array of current stage jobs APPEND(m_stage.jobs, m_job); } } return &this; }
El tercer argumento en ellos se transmite el puntero a la función de creación de parámetros de optimización de asesores expertos de etapa.
Clase COptimisationStage
Hay muchas propiedades en la descripción de esta clase en comparación con otras clases, pero esto solo se debe al hecho de que hay muchos campos en la base de datos de optimización en la tabla stages. Para cada una de ellas, existe una propiedad correspondiente en esta clase. Observe también que el puntero al objeto del proyecto que incluye esta etapa y el puntero al objeto de la etapa anterior se pasan al constructor de la etapa. Para la primera etapa no existe ninguna previa, por lo que para ella transmitiremos el valor NULL en este parámetro.
//+------------------------------------------------------------------+ //| Optimization stage class | //+------------------------------------------------------------------+ class COptimizationStage { public: ulong id_stage; ulong id_project; ulong id_parent_stage; string name; string expert; string symbol; string period; int optimization; int model; datetime from_date; datetime to_date; int forward_mode; datetime forward_date; int deposit; string currency; int profit_in_pips; int leverage; int execution_mode; int optimization_criterion; string status; COptimizationProject* project; COptimizationStage* parent_stage; COptimizationJob* jobs[]; COptimizationStage(ulong p_idStage, COptimizationProject* p_project, COptimizationStage* parentStage, string p_name, string p_expertName, string p_symbol = "GBPUSD", string p_timeframe = "H1", int p_optimization = 0, int p_model = 0, datetime p_fromDate = 0, datetime p_toDate = 0, int p_forwardMode = 0, datetime p_forwardDate = 0, int p_deposit = 10000, string p_currency = "USD", int p_profitInPips = 0, int p_leverage = 200, int p_executionMode = 0, int p_optimizationCriterion = 7, string p_status = "Done") : id_stage(p_idStage), project(p_project), id_project(!!p_project ? p_project.id_project : 0), parent_stage(parentStage), id_parent_stage(!!parentStage ? parentStage.id_stage : 0), name(p_name), expert(p_expertName), symbol(p_symbol), period(p_timeframe), optimization(p_optimization), model(p_model), from_date(p_fromDate), to_date(p_toDate), forward_mode(p_forwardMode), forward_date(p_forwardDate), deposit(p_deposit), currency(p_currency), profit_in_pips(p_profitInPips), leverage(p_leverage), execution_mode(p_executionMode), optimization_criterion(p_optimizationCriterion), status(p_status) {} // Create a stage in the database void Insert(); }; //+------------------------------------------------------------------+ //| Create a stage in the database | //+------------------------------------------------------------------+ void COptimizationStage::Insert() { string query = StringFormat("INSERT INTO stages VALUES(" "%s," // id_stage "%I64u," // id_project "%s," // id_parent_stage "'%s'," // name "'%s'," // expert "'%s'," // symbol "'%s'," // period "%d," // optimization "%d," // model "'%s'," // from_date "'%s'," // to_date "%d," // forward_mode "%s," // forward_date "%d," // deposit "'%s'," // currency "%d," // profit_in_pips "%d," // leverage "%d," // execution_mode "%d," // optimization_criterion "'%s'" // status ");", (id_stage == 0 ? "NULL" : (string) id_stage), // id_stage id_project, // id_project (id_parent_stage == 0 ? "NULL" : (string) id_parent_stage), // id_parent_stage name, // name expert, // expert symbol, // symbol period, // period optimization, // optimization model, // model TimeToString(from_date, TIME_DATE), // from_date TimeToString(to_date, TIME_DATE), // to_date forward_mode, // forward_mode (forward_mode == 4 ? "'" + TimeToString(forward_date, TIME_DATE) + "'" : "NULL"), // forward_date deposit, // deposit currency, // currency profit_in_pips, // profit_in_pips leverage, // leverage execution_mode, // execution_mode optimization_criterion, // optimization_criterion status // status ); PrintFormat(__FUNCTION__" | %s", query); id_stage = DB::Insert(query); }
Pero la composición de las acciones realizadas en el constructor y el método de inserción de un nuevo registro en la tabla stages muy sencilla: recordamos los valores transmitidos de los argumentos en las propiedades del objeto y los utilizamos para formar una consulta SQL para insertar un registro en la tabla requerida de la base de datos de optimización.
Clase COptimisationJob
Esta clase es idéntica en estructura a la clase COptimisationStage. El constructor recuerda los parámetros y el método Insert() inserta una nueva fila en la tabla jobs de la base de datos de optimización. Además, a cada objeto de trabajo, al crearse, se le transmite el puntero al objeto de etapa del que formará parte el objeto de trabajo.
//+------------------------------------------------------------------+ //| Optimization job class | //+------------------------------------------------------------------+ class COptimizationJob { public: ulong id_job; // job ID ulong id_stage; // stage ID string symbol; // Symbol string timeframe; // Timeframe string params; // Optimizer operation parameters string status; // Status COptimizationStage* stage; // Stage a job belongs to COptimizationTask* tasks[]; // Array of tasks related to the job // Constructor COptimizationJob(ulong p_jobId, COptimizationStage* p_stage, string p_symbol, string p_timeframe, string p_params, string p_status = "Done"); // Create a job in the database void Insert(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ COptimizationJob::COptimizationJob(ulong p_jobId, COptimizationStage* p_stage, string p_symbol, string p_timeframe, string p_params, string p_status = "Done") : id_job(p_jobId), stage(p_stage), id_stage(!!p_stage ? p_stage.id_stage : 0), symbol(p_symbol), timeframe(p_timeframe), params(p_params), status(p_status) {} //+------------------------------------------------------------------+ //| Create a job in the database | //+------------------------------------------------------------------+ void COptimizationJob::Insert() { // Request to create a second stage job for a given symbol and timeframe string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','%s');", id_stage, symbol, timeframe, params, status); id_job = DB::Insert(query); PrintFormat(__FUNCTION__" | %s -> %I64u", query, id_job); }
De la misma forma se construye la clase COptimisationTask restante, por lo que no ofreceremos su código aquí.
Reescribiendo CreateProject.mq5
Volvamos al archivo CreateProject.mq5 y veamos qué parámetros básicos tenemos allí. Este archivo se encuentra en la parte del proyecto, por lo que para cada proyecto individual podemos especificar los valores por defecto necesarios de los parámetros en él, de modo que no tengamos que cambiarlos al inicio.
En primer lugar, especificaremos el nombre de la base de datos de optimización:
input string fileName_ = "article.17328.db.sqlite"; // - Optimization database file
En el siguiente grupo de parámetros, especificaremos entre comas en qué símbolos y marcos temporales se optimizará el asesor experto de la primera y segunda etapa:
input string symbols_ = "GBPUSD,EURUSD,EURGBP"; // - Symbols input string timeframes_ = "H1,M30"; // - Timeframes
Esta selección creará seis trabajos para cada una de las combinaciones posibles de tres símbolos y dos marcos temporales.
El siguiente paso consistirá en seleccionar el intervalo en el que tendrá lugar la optimización:
input group "::: Project parameters - Optimization interval" input datetime fromDate_ = D'2022-09-01'; // - Start date input datetime toDate_ = D'2023-01-01'; // - End date
En el grupo de parámetros para la cuenta seleccionamos el símbolo principal que se usará en la tercera etapa, cuando el asesor experto trabaje en el probador con varios símbolos. Su elección adquiere importancia si entre los símbolos hay algunos cuya negociación continúe los fines de semana (por ejemplo, las criptodivisas). En este caso, deberemos elegir este como el principal, porque de lo contrario, el probador no generará ticks en todos los fines de semana al realizar la pasada.
input group "::: Project parameters - Account" input string mainSymbol_ = "GBPUSD"; // - Main symbol input int deposit_ = 10000; // - Initial deposit
El grupo de parámetros de la primera etapa especificaremos el nombre del asesor de la primera etapa, pero siempre se puede utilizar el mismo nombre. A continuación, indicaremos los criterios de optimización que se utilizarán para cada trabajo de la primera etapa. Solo serán números separados por comas. Un valor de 6 se corresponderá con el criterio de optimización definido por el usuario.
input group "::: Stage 1. Search" input string stage1ExpertName_ = "Stage1.ex5"; // - Stage EA input string stage1Criterions_ = "6,6,6"; // - Optimization criteria for tasks
En este caso, hemos especificado tres veces el criterio personalizado, por lo que cada trabajo contendrá tres problemas de optimización con el criterio especificado.
En el grupo de parámetros de la segunda etapa, hemos añadido la posibilidad de especificar todos los valores de los parámetros de la segunda etapa del EA, no solo el nombre y el número de estrategias del grupo. Estos parámetros influirán en la selección de los pasadas de la primera etapa, cuyos parámetros participarán en la selección de los grupos de la segunda etapa.
input group "::: Stage 2. Grouping" input string stage2ExpertName_ = "Stage2.ex5"; // - Stage EA input string stage2Criterion_ = "6"; // - Optimization criterion for tasks //input bool stage2UseClusters_= false; // - Use clustering? input double stage2MinCustomOntester_ = 500; // - Min value of norm. profit input uint stage2MinTrades_ = 20; // - Min number of trades input double stage2MinSharpeRatio_ = 0.7; // - Min Sharpe ratio input uint stage2Count_ = 8; // - Number of strategies in the group
Por ejemplo, si el valor es stage2MinTrades_ =20, solo podrán entrar en el grupo aquellas instancias únicas de estrategias comerciales que hayan realizado al menos 20 operaciones en la primera etapa. El parámetro stage2UseClusters_ se comenta por ahora, ya que no estamos usando la clusterización de los resultados de la etapa 2 en este momento. Por lo tanto, el valor false deberá sustituirse en su lugar.
En el grupo de parámetros de la tercera etapa, también hemos añadido algunas cosas. Además del nombre del asesor de la tercera etapa (tampoco se puede cambiar al cambiar de proyecto), existen dos parámetros que controlan la formación del nombre de la base de datos del asesor final. En el propio EA final, este nombre se forma en la función CVirtualAdvisor::FileName() según este patrón:
<Project name>-<Magic>.test.db.sqlite // To run in the tester <Project name>-<Magic>.db.sqlite // To run on a trading account
Por consiguiente, el asesor de la tercera fase utilizará la misma plantilla. Para la sustitución, en el lugar <Nombre del proyecto> se usa el parámetro projectName_, y en el lugar <Magic> se usa stage3Magic_. El parámetro stage3Tester_ se encarga de añadir el sufijo ".test".
input group "::: Stage 3. Result" input string stage3ExpertName_ = "Stage3.ex5"; // - Stage EA input ulong stage3Magic_ = 27183; // - Magic input bool stage3Tester_ = true; // - For the tester?
En principio, podríamos crear un parámetro que simplemente especifique el nombre completo de la base de datos del EA final. Una vez completado el tercer paso, podremos renombrar como deseemos el archivo resultante de esta base de datos antes de seguir utilizándolo.
Ahora solo necesitaremos crear las funciones para generar los parámetros de los asesores expertos por etapas según las plantillas dadas. Como vamos a usar tres etapas, necesitaremos tres funciones.
Para la primera etapa, la función tendrá el siguiente aspecto:
// Template of optimization parameters at the first stage string paramsTemplate1(COptimizationProject *p) { string params = StringFormat( "symbol_=%s\n" "period_=%d\n" "; === Open signal parameters\n" "signalSeqLen_=4||2||1||8||Y\n" "periodATR_=21||7||2||48||Y\n" "; === Pending order parameters\n" "stopLevel_=2.34||0.01||0.01||5.0||Y\n" "takeLevel_=4.55||0.01||0.01||5.0||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=15||1||1||30||Y\n", p.m_symbol, p.StringToTimeframe(p.m_timeframe)); return params; }
Se basa en los parámetros de la primera etapa de optimización del EA copiados del probador de estrategias con los rangos deseados de los parámetros de entrada individuales establecidos. En esta cadena se sustituyen los valores del símbolo y del marco temporal para los que se crea el objeto de trabajo en el proyecto al momento de llamar a esta función. Si, por ejemplo, para algún marco temporal se hace necesario utilizar otros rangos de parámetros de entrada a buscar, esta lógica podrá implementarse en esta función.
Al cambiar a otro proyecto con una estrategia comercial diferente, esta función deberá sustituirse por otra escrita para la nueva estrategia comercial y su conjunto de parámetros de entrada.
Para la segunda y tercera etapa, también hemos escrito la implementación de estas funciones en el archivo CreateProject.mq5, no obstante, al cambiar a otro proyecto, lo más probable es que no tengamos que cambiarlas. Pero no las llevaremos directamente a la sección de la biblioteca, por ahora se quedarán aquí:
// Template of optimization parameters for the second stage string paramsTemplate2(COptimizationProject *p) { // Find the parent job ID for the current job // by matching the symbol and timeframe at the current and parent stages int i; SEARCH(p.m_stage.parent_stage.jobs, (p.m_stage.parent_stage.jobs[i].symbol == p.m_symbol && p.m_stage.parent_stage.jobs[i].timeframe == p.m_timeframe), i); ulong parentJobId = p.m_stage.parent_stage.jobs[i].id_job; string params = StringFormat( "idParentJob_=%I64u\n" "useClusters_=%s\n" "minCustomOntester_=%f\n" "minTrades_=%u\n" "minSharpeRatio_=%.2f\n" "count_=%u\n", parentJobId, (string) false, //(string) stage2UseClusters_, stage2MinCustomOntester_, stage2MinTrades_, stage2MinSharpeRatio_, stage2Count_ ); return params; } // Template of optimization parameters at the third stage string paramsTemplate3(COptimizationProject *p) { string params = StringFormat( "groupName_=%s\n" "advFileName_=%s\n" "passes_=\n", StringFormat("%s_v.%s_%s", p.name, p.version, TimeToString(toDate_, TIME_DATE)), StringFormat("%s-%I64u%s.db.sqlite", p.name, stage3Magic_, (stage3Tester_ ? ".test" : ""))); return params; }
Luego tenemos el código de la función de inicialización, que realiza todo el trabajo y elimina este asesor experto del gráfico antes de que finalice. Vamos a mostrarlo ahora con los parámetros de las funciones llamadas:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Create an optimization project object for the given database COptimizationProject p(fileName_); // Create a new project in the database p.Create(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE))); // Add the first stage p.AddStage(NULL, "First", stage1ExpertName_, mainSymbol_, "H1", 2, 2, fromDate_, toDate_, 0, 0, deposit_); // Adding the first stage jobs p.AddJobs(symbols_, timeframes_, paramsTemplate1); // Add tasks for the first stage jobs p.AddTasks(stage1Criterions_); // Add the second stage p.AddStage(p.m_stages[0], "Second", stage2ExpertName_, mainSymbol_, "H1", 2, 2, fromDate_, toDate_, 0, 0, deposit_); // Add the second stage jobs p.AddJobs(symbols_, timeframes_, paramsTemplate2); // Add tasks for the second stage jobs p.AddTasks(stage2Criterion_); // Add the third stage p.AddStage(p.m_stages[1], "Save to library", stage3ExpertName_, mainSymbol_, "H1", 0, 2, fromDate_, toDate_, 0, 0, deposit_); // Add the third stage job p.AddJobs(mainSymbol_, "H1", paramsTemplate3); // Add a task for the third stage job p.AddTasks("0"); // Put the project in the execution queue p.Queue(); // Delete the EA ExpertRemove(); // Successful initialization return(INIT_SUCCEEDED); }
Esta parte del código también se puede dejar sin cambios cuando pasemos a otro proyecto, a menos que queramos cambiar la composición de los pasos del transportador de optimización automática. Con el tiempo, también lo mejoraremos. Por ejemplo, ahora el código contiene constantes numéricas que deberían sustituirse por constantes con nombre para mejorar la legibilidad. Si resulta que este código realmente no necesita ningún cambio, lo pasaremos a la parte de la biblioteca.
Así pues, el EA para crear proyectos de optimización en la base de datos ya está listo. Ahora crearemos los asesores de etapas.
Asesores de etapas
Ya hemos creado el primer asesor de la etapa Stage1.mq5 en la última parte, por eso ahora hemos introducido en él cambios relacionados exclusivamente con la adición del nuevo parámetro maxSpread_ a la estrategia comercial. Estos cambios ya se han comentado anteriormente.
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; // 3. Connect the general part of the first stage EA from the Advisor library #include <antekov/Advisor/Experts/Stage1.mqh> //+------------------------------------------------------------------+ //| 4. Strategy inputs | //+------------------------------------------------------------------+ sinput string symbol_ = ""; // Symbol sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; // Timeframe for candles input group "=== Opening signal parameters" input int signalSeqLen_ = 6; // Number of unidirectional candles input int periodATR_ = 0; // ATR period (if 0, then TP/SL in points) input group "=== Pending order parameters" input double stopLevel_ = 25000; // Stop Loss (in ATR fraction or points) input double takeLevel_ = 3630; // Take Profit (in ATR fraction or points) input group "=== Money management parameters" input int maxCountOfOrders_ = 9; // Max number of simultaneously open orders input int maxSpread_ = 10; // Max acceptable spread (in points) //+------------------------------------------------------------------+ //| 5. Strategy initialization string generation function | //| from the inputs | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); }
En el asesor experto de la segunda y tercera etapa, solo necesitaremos definir la constante __NAME__ con el nombre único del asesor experto y conectar el archivo o archivos de las estrategias comerciales utilizadas. El resto del código se tomará del archivo de inclusión de la biblioteca de la etapa correspondiente. Este es el aspecto que podría tener el código del asesor de la segunda etapa Stage2.mq5:
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Stage2.mqh>
y Stage3.mq5:
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Stage3.mqh>
Asesor final
En el EA final solo tendremos que añadir la conexión de la estrategia utilizada. Aquí no deberá declararse la constante __NAME__, porque en este caso tanto ella como la función de formación de la cadena de inicialización se declararán en el archivo de inclusión desde la parte de la biblioteca. En el código siguiente, en los comentarios hemos mostrado cuál será en este caso el nombre de EA y la función de generación de la cadena de inicialización:
// 1. Define a constant with the EA name //#define __NAME__ MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Expert.mqh> //+------------------------------------------------------------------+ //| Function for generating the strategy initialization string | //| from the default inputs (if no name was specified). | //| Import the initialization string from the EA database | //| by the strategy group ID | //+------------------------------------------------------------------+ //string GetStrategyParams() { //// Take the initialization string from the new library for the selected group //// (from the EA database) // string strategiesParams = CVirtualAdvisor::Import( // CVirtualAdvisor::FileName(__NAME__, magic_), // groupId_ // ); // //// If the strategy group from the library is not specified, then we interrupt the operation // if(strategiesParams == NULL && useAutoUpdate_) { // strategiesParams = ""; // } // // return strategiesParams; //}
Si de repente queremos cambiar algo de este código, bastará con descomentar este código y hacerle las modificaciones necesarias.
Así, tendremos los siguientes archivos en la parte del proyecto:

Ahora compilaremos todos los archivos de la parte del proyecto de forma que por cada archivo con extensión mq5 se cree un archivo con extensión ex5.
Montamos todo junto
Primer paso. Creación del proyecto
Para ello, arrastramos el asesor experto CreateProject.ex5 a cualquier gráfico en el terminal (¡este asesor experto no debe ejecutarse en el probador!). En el código fuente de este asesor experto ya hemos intentado especificar valores reales para todos los parámetros de entrada, por lo que podemos simplemente hacer clic en OK en el cuadro de diálogo que aparecerá.

Figura 1. Inicio del asesor para la creación de un proyecto en la base de datos de optimización
Como resultado, tendremos un archivo article.17328.db.sqlite con la base de datos de optimización en la carpeta común del terminal.
Segundo paso. Inicio de la optimización
Arrastramos el asesor experto Optimisation.ex5 a cualquier gráfico (¡este asesor experto tampoco necesita ejecutarse en el probador!). En el cuadro de diálogo que se abrirá, permitimos el uso de DLL, y en la pestaña de parámetros, comprobamos que tenemos el nombre correcto de la base de datos de optimización:


Figura 2. Ejecución del asesor experto de optimización automática
Si todo está en orden, deberíamos ver aproximadamente la siguiente imagen: el probador ejecutará la primera etapa de optimización del EA en el primer par símbolo-marco temporal, y el gráfico con el EA Optimization.ex5 en ejecución mostrará "Total tasks in queue: ..., Current Task ID: ...".

Figura 3. Funcionamiento del asesor de optimización automática.
A continuación, deberemos esperar un tiempo hasta que se completen todas las tareas de optimización. Este tiempo puede resultar bastante significativo si el intervalo de prueba es largo y el número de símbolos y marcos temporales es grande. Con la configuración actual por defecto de 33 agentes, el proceso completo ha durado unas cuatro horas.
En la última etapa de la tubería, ya no se realiza la optimización, sino que se activa una única pasada del asesor de la tercera etapa. Esto creará un archivo con la base de datos del EA final. Como hemos elegido el nombre del proyecto "SimpleCandles", un número mágico igual a 27183 y un valor del parámetro de entrada stage3Tester_=true al crear el proyecto, se creará un archivo con el nombre SimpleCandles-27183.test.db.sqlite en la carpeta común del terminal.
Paso 3. Ejecutamos el EA final en el probador
Vamos a intentar ejecutar el EA final en el probador. Como ahora su código se toma completamente de la parte de la biblioteca, los valores por defecto de los parámetros se definirán allí también. Así, al ejecutar el asesor experto SimpleCandles.ex5 en el probador sin cambiar los valores de los parámetros de entrada, utilizará el último grupo de estrategias añadido (groupId_= 0) con la actualización automática habilitada (useAutoUpdate_= true) desde el archivo de básico de datos llamado SimpleCandles-27183.test.db.sqlite (el nombre de archivo del asesor experto SimpleCandles, más el número mágico por defecto magic_= 27183 y más el sufijo ".test" por ejecutarlo en el probador).
Desafortunadamente, aún no hemos creado ninguna herramienta especial que permita ver los identificadores de grupo de estrategias existentes en la base de datos del EA final. Solo podemos abrir la propia base de datos en cualquier editor SQLite y verlos en la tabla strategy_groups.
Sin embargo, si solo hemos creado un proyecto de optimización y se ha ejecutado una vez, en la base de datos del asesor experto final solo aparecerá un grupo de estrategias con el identificador 1. Por lo tanto, en cuanto a la selección de grupo, no existirá diferencia si ahora indicamos el identificador específico groupId_= 1 o dejamos groupId_= 0. En cualquier caso, se cargará el único grupo existente. Si ejecutamos el mismo proyecto repetidamente (esto se puede hacer cambiando el estado del proyecto directamente en la base de datos) o hacemos otro y lo ejecutamos, aparecerán nuevos grupos de estrategias en la base de datos del EA final. Entonces, con diferentes valores del parámetro groupId_ , ya se utilizarán diferentes grupos.
El parámetro para activar la actualización automática (useAutoUpdate_= true) también requiere nuestra atención. A pesar de tener solo un grupo, este parámetro afecta al rendimiento del EA final. Esto se manifiesta en el hecho de que, cuando está activada la actualización automática, solo pueden cargarse para trabajar los grupos de estrategias cuya fecha de aparición sea inferior a la fecha actual modelada.
Esto significa que si ejecutamos el EA final en el mismo intervalo que hemos usado para la optimización (2022.09.01 - 2023.01.01), nuestro único grupo de estrategias no se cargará, porque tiene una fecha de formación de 2023.01.01. Por ello, tendremos que desactivar la actualización automática (useAutoUpdate_= false) y especificar un identificador específico del grupo de estrategias comerciales utilizado (groupId_= 1) en los parámetros de entrada al iniciar el EA final, o seleccionar otro intervalo situado después de la fecha de finalización del intervalo de optimización.
En general, hasta que no hayamos elegido finalmente qué estrategias se utilizarán en el asesor experto final, y no nos hayamos fijado el objetivo de probarlas para la conveniencia de una reoptimización periódica, podremos establecer este parámetro en false e indicar un identificador específico del grupo de estrategias comerciales utilizado.
El último conjunto de parámetros importantes es responsable del nombre de la base de datos que usará el EA final. En su configuración por defecto, el número mágico es el mismo que el que indicamos en la configuración al crear el proyecto. También hemos hecho que el nombre del archivo de la EA final coincida con el nombre del proyecto. Y el valor del parámetro stage3Tester_, al crearse el proyecto, era igual a true, por lo que el nombre de archivo de la base de datos creada del EA final será SimpleCandles-27183.test.db.sqlite. Eso coincide totalmente con el que usará el asesor experto final SimpleCandles.ex5.
Veamos los resultados de la ejecución del EA final en el intervalo de optimización:


Figura 4. Funcionamiento del asesor experto de optimización automática en el intervalo 2022.09.01 - 2023.01.01
Si lo ejecutamos en otro marco temporal, los resultados probablemente no serán tan bonitos:


Figura 5. Funcionamiento del asesor experto de optimización automática en el intervalo 2023.01.01 - 2023.02.01
Tomaremos como ejemplo un intervalo de un mes inmediatamente posterior al intervalo de optimización. De hecho, la reducción ha sido ligeramente superior al valor esperado del 10% y el beneficio normalizado ha disminuido unas cinco veces. ¿Es posible volver a ejecutar la optimización en los últimos tres meses y obtener un patrón similar de comportamiento del EA durante el mes siguiente? Esta cuestión sigue abierta por el momento.
Paso 4. Ejecutamos el asesor experto final en una cuenta comercial
Para ejecutar el EA final en una cuenta comercial, necesitaremos ajustar el nombre del archivo de base de datos que hemos obtenido. Deberemos quitarle el sufijo ".test". Es decir, simplemente renombraremos o copiaremos SimpleCandles-27183.test.db.sqlite en SimpleCandles-27183.db.sqlite. Su ubicación seguirá siendo la misma: en la carpeta común del terminal.
Luego arrastraremos y soltaremos el asesor experto final SimpleCandles.ex5 en cualquier gráfico de terminal. En los parámetros de entrada podemos dejar todo con los valores por defecto, ya que nos conformamos con cargar el último grupo de estrategias, y la fecha actual será obviamente mayor que la fecha de creación de este grupo.

Figura 6. Parámetros de entrada por defecto para la EA final
Mientras escribía el texto del artículo, este EA final tuvo tiempo de trabajar en una cuenta demo durante aproximadamente una semana y mostró estos resultados:

Figura 7. Resultados del asesor experto final en la cuenta comercial
Ha sido una semana bastante buena para el EA. Con una reducción del 1,27%, el beneficio ha sido de aproximadamente el 2%. Un par de veces el asesor experto se ha reiniciado porque la propia computadora lo hizo, pero ha restaurado con éxito la información sobre las posiciones virtuales abiertas y ha continuado funcionando.
Conclusión
Veamos qué tenemos. Por fin hemos reunido en algo parecido a un sistema coherente los resultados de un proceso de desarrollo bastante largo. La herramienta obtenida para la organización de la optimización automática y la comprobación de estrategias comerciales permite mejorar los resultados de la comprobación incluso de estrategias comerciales sencillas mediante la diversificación usando diferentes instrumentos comerciales.
También permite reducir en gran medida el número de operaciones que requieren intervención manual para lograr los mismos objetivos. Ahora no tenemos necesidad de rastrear el momento en que termina el siguiente proceso de optimización para iniciar el siguiente; tampoco hay necesidad de pensar en cómo guardar los resultados intermedios de las optimizaciones y cómo integrarlos en un asesor comercial. Podemos centrarnos directamente en desarrollar la lógica de las estrategias comerciales.
Obviamente, aún queda mucho por hacer para mejorar y facilitar el uso de esta herramienta. La idea de una interfaz web completa que gestione no solo la creación, el lanzamiento y la supervisión de los proyectos de optimización en curso, sino también el funcionamiento de los asesores expertos que se ejecutan en diferentes terminales y la visualización de sus estadísticas sigue estando en nuestros planes lejanos. Se trata de una tarea muy laboriosa, pero mirando hacia atrás, podemos decir lo mismo de la tarea que hoy ya ha recibido una solución más o menos completa.
Gracias por su atención, ¡hasta pronto!
Advertencia importante
Todos los resultados expuestos en este artículo y en todos los artículos anteriores de la serie se basan únicamente en datos de pruebas históricas y no ofrecen ninguna garantía de lograr beneficios en el futuro. El trabajo de este proyecto es de carácter exploratorio. Todos los resultados publicados pueden ser usados por cualquiera bajo su propia responsabilidad.
Contenido del archivo
| # | Nombre | Versión | Descripción | Cambios recientes |
|---|---|---|---|---|
| MQL5/Experts/Article.17328 | Carpeta de trabajo del proyecto | |||
| 1 | CreateProject.mq5 | 1.02 | Script asesor para crear un proyecto con etapas, actividades y tareas de optimización. | Parte 25 |
| 2 | Optimization.mq5 | 1.00 | Asesor experto para la optimización automática de proyectos | Parte 23 |
| 3 | SimpleCandles.mq5 | 1.01 | Asesor experto final para el funcionamiento en paralelo de varios grupos de estrategias modelo. Los parámetros se tomarán de la biblioteca de grupos incorporada. | Parte 25 |
| 4 | Stage1.mq5 | 1.02 | Asesor experto para optimizar una única instancia de una estrategia comercial (Etapa 1) | Parte 25 |
| 5 | Stage2.mq5 | 1.01 | Asesor experto para optimizar un grupo de instancias de estrategias comerciales (Etapa 2) | Parte 25 |
| 6 | Stage3.mq5 | 1.01 | Asesor experto que guarda un grupo normalizado formado de estrategias usando como base los datos del asesor experto con el nombre especificado. | Parte 25 |
| MQL5/Experts/Article.17328/Strategies | Carpeta de estrategias del proyecto | |||
| 7 | SimpleCandlesStrategy.mqh | 1.01 | Clase de la estrategia comercial SimpleCandles | Parte 25 |
| MQL5/Include/antekov/Advisor/Base | Clases básicas de las que heredan otras clases del proyecto | |||
| 8 | Advisor.mqh | 1.04. | Clase básica del experto | Parte 10 |
| 9 | Factorable.mqh | 1.05 | Clase básica de objetos creados a partir de una cadena (string) | Parte 24 |
| 10 | FactorableCreator.mqh | 1.00 | Parte 24 | |
| 11 | Interface.mqh | 1.01 | Clase básica de visualización de diversos objetos | Parte 4 |
| 12 | Receiver.mqh | 1.04. | Clase básica de transferencia de volúmenes abiertos a posiciones de mercado | Parte 12 |
| 13 | Strategy.mqh | 1.04. | Clase básica de estrategia comercial | Parte 10 |
| MQL5/Include/antekov/Advisor/Database | Archivos para trabajar con todo tipo de bases de datos utilizadas por los asesores de proyectos | |||
| 14 | Database.mqh | 1.12 | Clase para trabajar con la base de datos | Parte 25 |
| 15 | db.adv.schema.sql | 1.00 | Esquema de la base de datos del asesor final | Parte 22 |
| 16 | db.cut.schema.sql | 1.00 | Esquema de una base de datos de optimización truncada | Parte 22 |
| 17 | db.opt.schema.sql | 1.05 | Esquema de la base de datos de optimización | Parte 22 |
| 18 | Storage.mqh | 1.01 | Clase de trabajo con almacenamiento Key-Value para el asesor experto final en la base de datos del asesor experto. | Parte 23 |
| MQL5/Include/antekov/Advisor/Experts | Archivos con partes comunes de asesores usados de distintos tipos | |||
| 19 | Expert.mqh | 1.22 | Archivo de biblioteca para el asesor final. Los parámetros del grupo pueden tomarse de la base de datos del asesor experto | Parte 23 |
| 20 | Optimization.mqh | 1.04. | Archivo de biblioteca para el asesor experto que controla el inicio de las tareas de optimización | Parte 23 |
| 21 | Stage1.mqh | 1.19 | Archivo de biblioteca del asesor experto para optimizar una única instancia de una estrategia comercial (Etapa 1) | Parte 23 |
| 22 | Stage2.mqh | 1.04. | Archivo de biblioteca del asesor experto para optimizar un grupo de instancias de estrategias comerciales (Etapa 2) | Parte 23 |
| 23 | Stage3.mqh | 1.04. | Archivo de biblioteca para el asesor experto que guarda el grupo normalizado generado de estrategias en la base de datos del asesor experto con un nombre especificado. | Parte 23 |
| MQL5/Include/antekov/Advisor/Optimization | Clases responsables del trabajo de optimización automática | |||
| 24 | OptimizationJob.mqh | 1.00 | Clase para el funcionamiento de la etapa de proyecto de optimización | Parte 25 |
| 25 | OptimizationProject.mqh | 1.00 | Clase para el proyecto de optimización | Parte 25 |
| 26 | OptimizationStage.mqh | 1.00 | Clase para la fase del proyecto de optimización | Parte 25 |
| 27 | OptimizationTask.mqh | 1.00 | Clase para la tarea de optimización (para la creación) | Parte 25 |
| 28 | Optimizer.mqh | 1.03 | Clase para el gestor de optimización automática de proyectos | Parte 22 |
| 29 | OptimizerTask.mqh | 1.03 | Clase para la tarea de optimización (para el transportador) | Parte 22 |
| MQL5/Include/antekov/Advisor/Strategies | Ejemplos de estrategias comerciales usadas para demostrar el trabajo del proyecto | |||
| 30 | HistoryStrategy.mqh | 1.00 | Clase de estrategia comercial para reproducir la historia de transacciones | Parte 16 |
| 31 | SimpleVolumesStrategy.mqh | 1.11 | Clase de estrategia comercial con uso de volúmenes de ticks | Parte 22 |
| MQL5/Include/antekov/Advisor/Utils | Utilidades auxiliares, macros para la reducción del código | |||
| 32 | ExpertHistory.mqh | 1.00 | Clase para exportar la historia de transacciones a un archivo | Parte 16 |
| 33 | Macros.mqh | 1.06 | Macros útiles para operaciones con arrays | Parte 25 |
| 34 | NewBarEvent.mqh | 1.00 | Clase de definición de una nueva barra para un símbolo específico | Parte 8 |
| 35 | SymbolsMonitor.mqh | 1.00 | Clase de obtención de información sobre instrumentos comerciales (símbolos) | Parte 21 |
| MQL5/Include/antekov/Advisor/Virtual | Clases para crear diversos objetos unidos mediante un sistema de órdenes y posiciones comerciales virtuales | |||
| 36 | Money.mqh | 1.01 | Clase básica de gestión de capital | Parte 12 |
| 37 | TesterHandler.mqh | 1.07 | Clase para gestionar los eventos de optimización | Parte 23 |
| 38 | VirtualAdvisor.mqh | 1.10 | Clase de asesor experto para trabajar con posiciones (órdenes) virtuales | Parte 24 |
| 39 | VirtualChartOrder.mqh | 1.01 | Clase de posición virtual gráfica | Parte 18 |
| 40 | VirtualHistoryAdvisor.mqh | 1.00 | Clase experta para reproducir la historia de transacciones | Parte 16 |
| 41 | VirtualInterface.mqh | 1.00 | Clase de GUI del asesor | Parte 4 |
| 42 | VirtualOrder.mqh | 1.09 | Clase de órdenes y posiciones virtuales | Parte 22 |
| 43 | VirtualReceiver.mqh | 1.04. | Clase de transferencia de los volúmenes abiertos a las posiciones de mercado (receptor) | Parte 23 |
| 44 | VirtualRiskManager.mqh | 1.05 | Clase de gestión de riesgos (gestor de riesgos) | Parte 24 |
| 45 | VirtualStrategy.mqh | 1.09 | Clase de estrategia comercial con posiciones virtuales | Parte 23 |
| 46 | VirtualStrategyGroup.mqh | 1.03 | Clase de grupo o grupos de estrategias comerciales | Parte 24 |
| 47 | VirtualSymbolReceiver.mqh | 1.00 | Clase de receptor simbólico | Parte 3 |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/17328
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Operar con el Calendario Económico MQL5 (Parte 6): Automatizar la entrada de operaciones con análisis de noticias y temporizadores de cuenta regresiva
Introducción a MQL5 (Parte 13): Guía para principiantes sobre cómo crear indicadores personalizados (II)
Redes neuronales en el trading: Detección adaptativa de anomalías del mercado (Final)
Introducción a MQL5 (Parte 11): Guía de trabajo con indicadores incorporados en MQL5 para principiantes (II)
- 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
En primer lugar, me gustaría saber en qué idioma está esto.
Es coreano. Tu navegador no lo muestra por alguna razón.
Es coreano. Tu navegador no lo muestra por alguna razón.
Exactamente. No publiqué nada en este hilo ese día, 2025.07.08 desde el principio. Si sigues el enlace al hilo, aparece un post con una fecha diferente. Probablemente también sea culpa de mi navegador que los programadores que te quedan no sepan hacerlo bien.
Exactamente. No publiqué nada en este hilo ese día, 2025.07.08 desde el principio. Si sigues este enlace al hilo, aparece un post con una fecha diferente. Probablemente también sea culpa de mi navegador que los programadores que te quedan no puedan seguir el ritmo.
Gracias por tu insistencia, arreglado.
Gracias por su persistencia, corregido.
Perdón por la persistencia, no veo la solución. El enlace sigue llevando a un extraño mensaje que yo no he escrito. Bueno, incluso si suponemos que lo escribí yo, ¿por qué no hay ningún mensaje en ruso al lado? O crees que si no puedo aprender inglés, aprendí coreano y me estoy divirtiendo....
Esa es la diferencia en una discusión en diferentes idiomas.
Esto es del enlace.
Esta es la traducción al ruso.
Y esto es lo que hay en la versión rusa del artículo.
Entonces, ¿en qué idioma estaba intentando escribir? ????
Todo es un solo tema. Y si te fijas en los demás, encontrarás mensajes de origen extraño en idiomas con los que nunca soñé.
Puede que haya exagerado. Sólo he encontrado otro mensaje similar, en inglés y probablemente una traducción real.
Por favor, borre el mensaje anterior en todas las versiones lingüísticas y probablemente será corregido. Tal vez no completamente como la última vez.......
Foro sobre trading, sistemas automatizados de trading y prueba de estrategias de trading
Discusión del artículo "Desarrollo de un Asesor Experto multidivisa (Parte 25): Enchufando una nueva estrategia (II)"
Rashid Umarov, 2025.07.06 14:04
Gracias, lo solucionaremos.
Ya hemos resuelto este problema, pero parece que no está completamente resuelto.