English Русский Deutsch 日本語 Português
preview
Desarrollamos un asesor experto multidivisa (Parte 18): Automatización de la selección de grupos considerando el periodo forward

Desarrollamos un asesor experto multidivisa (Parte 18): Automatización de la selección de grupos considerando el periodo forward

MetaTrader 5Probador | 20 diciembre 2024, 09:38
298 0
Yuriy Bykov
Yuriy Bykov

Introducción

En un artículo anterior de esta serie (en la parte 7), analizamos la selección de un grupo de instancias individuales de estrategias comerciales para mejorar el rendimiento cuando estas trabajan juntas. Para la selección, utilizamos dos métodos. En el primer enfoque, la selección de grupos se realizó usando los resultados de optimización obtenidos durante todo el intervalo temporal de optimización. En el grupo tratamos de tomar aquellas instancias únicas que mostraban los mejores resultados en el intervalo de optimización. En el segundo enfoque, extrajimos un pequeño fragmento del intervalo temporal de optimización, en el que no se realizó ninguna optimización de instancia única. El fragmento seleccionado del intervalo temporal se utilizó entonces en la selección de grupos: intentamos incluir en el grupo aquellas instancias individuales que mostraban buenos resultados (pero no los mejores) en el intervalo de optimización y que, al mismo tiempo, mostraban aproximadamente los mismos resultados en el fragmento seleccionado del intervalo temporal.

Al final, los resultados fueron los siguientes: 

  • No observamos una ventaja clara de la selección con el primer método sobre el segundo. Esto puede deberse al corto intervalo temporal de la historia en el que comparamos los resultados de ambos métodos. Aun así, tres meses no es tiempo suficiente para evaluar una estrategia que pueda tener periodos más largos de estancamiento.
  • El segundo método ha demostrado que en el fragmento de intervalo temporal seleccionado los resultados son mejores si aplicamos la selección en un grupo según el algoritmo de búsqueda de instancias únicas de estrategias comerciales que estén próximas entre sí en cuanto a los resultados. Si los seleccionamos simplemente según el mejor rendimiento posible en el intervalo de optimización (como en el primer método, pero solo en un intervalo más corto), los resultados del grupo seleccionado resultan notablemente peores.
  • Podemos combinar ambos métodos, es decir, construir dos grupos seleccionados con métodos diferentes y luego combinar los dos grupos obtenidos en uno solo.

En la parte 13, implementamos la automatización de la segunda etapa de optimización, que implicaba la selección de instancias individuales de estrategias comerciales obtenidas en la primera etapa en un grupo. Para ello, aplicamos una búsqueda simple utilizando el algoritmo genético del optimizador interno en el simulador de estrategias. Aún no hemos realizado ninguna clasificación previa de instancias individuales (tema tratado en la parte 6). De esta forma, hemos automatizado la selección de grupos de la primera manera. Por ahora no habíamos llegado a la aplicación de la selección de grupos de la segunda manera, pero ahora es el momento de volver a este tema. En el marco de este artículo trataremos de implementar la selección automatizada de instancias individuales de estrategias comerciales en grupos, teniendo en cuenta su comportamiento en el periodo forward.


Trazando el camino

Como siempre, primero veremos lo que ya tenemos y lo que nos falta para la tarea a realizar. Podemos fijarnos la tarea de optimizar una estrategia comercial en cualquier intervalo temporal que deseemos. Las palabras "fijarnos la tarea" deben tomarse al pie de la letra: para ello, crearemos las entradas necesarias en la tabla (tasks) de nuestra base de datos. En consecuencia, realizar hacer primero la optimización en un intervalo temporal (por ejemplo, de 2018 a 2022, ambos inclusive) y después en otro intervalo (por ejemplo, para 2023).

Pero con este planteamiento no podremos utilizar los resultados de la forma deseada. En cada uno de los dos intervalos temporales, la optimización se realizará de forma independiente, por lo que no habrá nada que comparar entre ellas: las pasadas de la segunda optimización no repetirán las pasadas de la primera en cuanto a los valores de los parámetros de entrada. Y lo anterior resulta válido para la optimización genética que utilizaremos. Está claro que esto no es cierto para la optimización completa, pero nunca la hemos usado y probablemente no la utilizaremos en el futuro debido al gran número de combinaciones de parámetros optimizados.

Por ello, tendremos que implicar la ejecución del proceso de optimización con el periodo forward especificado. En este caso, el simulador usará las mismas combinaciones de parámetros de entrada en el periodo anterior que en el periodo principal. Pero aún no hemos probado a ejecutar la optimización automática con un periodo forward, y no sabemos cómo llegarán estos resultados a nuestra base de datos. ¿Podremos distinguir entonces entre pasadas dentro del periodo principal y pasadas en el periodo anterior? Tendremos que comprobarlo.

Una vez estemos seguros de que disponemos en la base de datos de toda la información necesaria sobre las pasadas tanto para el periodo principal como para el periodo forward, podremos pasar a la siguiente etapa. En la parte 7, una vez obtenidos estos resultados, los analizaremos y cribaremos manualmente utilizando Excel para ello. Sin embargo, en el contexto de la automatización, su uso parece ineficiente. Aun así, intentaremos evitar cualquier manipulación manual de los datos durante la obtención del EA final. Afortunadamente, todas las acciones que efectuamos en Excel (recálculo de algunos resultados, cálculo de relaciones de indicadores para diferentes periodos de prueba, búsqueda de la puntuación final para cada grupo de estrategias y clasificación según esta) se pueden realizar en el programa MQL5 usando consultas SQL a nuestra base de datos o ejecutando un script en Python.

Una vez clasificados según la puntuación final, solo tomaremos el grupo más alto en el EA final. Realizaremos acciones similares para todas las combinaciones de símbolos y marcos temporales seleccionados. Tras normalizar el grupo global para incluir los mejores grupos para todos los pares símbolo-marco temporal, el EA final estará listo.

Vamos a comenzar con la implementación, pero primero corregiremos un error que hemos descubierto.


Solucionamos un error de almacenamiento

Al desarrollar el EA para automatizar la primera etapa (optimización de instancias individuales de estrategias comerciales), usamos una sola base de datos. Así que no había duda de qué base de datos debíamos recuperar o en qué base guardar los datos. En la segunda fase de optimización, añadimos una nueva base de datos auxiliar que contenía el mínimo necesario de esencia de la base de datos principal. Esta versión reducida de la base de datos se envió a los agentes de pruebas en el marco de la segunda fase de optimización.

Pero debido al enfoque ya elegido al implementar la clase de base de datos estática, tuvimos que utilizar una solución algo incómoda que nos permitiera cambiar el nombre de la base de datos si fuera necesario. Después de cambiar el nombre, todas las llamadas posteriores al método de conexión a la base de datos utilizaban el nuevo nombre. De ahí el error al sumar los resultados de la pasada en la segunda y la tercera etapa. La razón era la falta de conmutación de regreso a la base principal en todos los lugares donde era necesario.

Para solucionarlo, hemos añadido a cada asesor de etapas y al asesor de optimización automática de proyectos un parámetro de entrada adicional que especifica el nombre de la base de datos principal. Además de eliminar el error, esto resulta útil porque podemos separar mejor las bases de datos que se utilizan en diferentes artículos. Por ejemplo, en esta parte se utilizaba una nueva base de datos maestra porque decidimos reducir la composición de los problemas de optimización, pero no queríamos limpiar la base de datos existente:

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput string fileName_   = "database683.sqlite"; // - File with the main database


En la función OnInit() del asesor experto de la segunda etapa SimpleVolumesStage2.mq5, dentro de la llamada de la función LoadParams() había una conexión a la base de datos auxiliar, porque los datos sobre los parámetros de entrada de las instancias individuales de estrategias comerciales para la conexión al grupo deben tomarse de ella. Tras finalización la pasada, se llamaba a la función OnTester(), en la que se debía realizar el guardado de los resultados de la pasada del grupo ya en la base de datos principal. Pero como no se volvía a la base de datos principal, los resultados completos de la pasada (48 columnas) intentaban insertarse en una tabla de la base de datos auxiliar (2 columnas).

Por lo tanto, añadimos el interruptor faltante a la base de datos principal en la función OnInit() del EA de la segunda etapa SimpleVolumesStage2.mq5:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {

      ... 

// Load strategy parameter sets
   string strategiesParams = LoadParams(indexes);

// Connect to the main database
   DB::Connect(fileName_);
   DB::Close();


     ... 

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}


En los asesores expertos de optimización de la primera y la tercera etapa que no utilizan la base de datos auxiliar, añadimos a la primera llamada del método de conexión a la base de datos el nombre de la base de datos tomado del nuevo parámetro de entrada del asesor experto:

DB::Connect(fileName_)


Otro tipo de error que encontramos se producía cuando, una vez terminada, queríamos ejecutar una de nuestras pasadas favoritas por separado. La pasada se iniciaba y ejecutaba con normalidad, pero sus resultados no se introducían en la base de datos. La razón era que en ese inicio el identificador de la tarea seguía siendo igual a 0, mientras que en la base de datos solo se puede escribir en la tabla passes una fila con el identificador de una tarea existente en la tabla tasks.

Esto puede corregirse haciendo que el ID de la tarea tome un valor de los parámetros de entrada del asesor experto (de donde proviene cuando se optimiza), o añadiendo una tarea ficticia con un ID 0 a la base de datos. Al final, hemos elegido la segunda opción para que nuestras pasadas individuales ejecutadas manualmente no se contabilicen como pasadas realizadas como parte de una tarea de optimización concreta. Para la tarea ficticia añadida, ha sido necesario especificar cualquier ID de una tarea existente para no violar las restricciones de las claves externas y el estado "Done" para que esta tarea no se ejecutara durante la optimización automática.

Una vez hechas estas correcciones, podemos volver a la tarea principal.


Preparación del código y la base de datos

Tomaremos una copia de una base de datos existente y la limpiaremos de datos sobre pasadas, tareas y trabajos. A continuación, modificaremos los datos de la primera etapa añadiendo la fecha de inicio del periodo forward. La segunda etapa de la tabla (stages) podemos eliminarla. Ahora crearemos una entrada en la tabla tasks para la primera etapa, especificando el símbolo y el periodo (EURGBP H1) y los parámetros para el simulador de estrategias. En ellos incluiremos la optimización sobre un solo parámetro para que el número de pasadas sea pequeño. Esto nos permitirá obtener resultados de forma más rápida. Para el trabajo creado, añadiremos una tarea con un criterio de optimización complejo a la tabla tasks.

Ahora vamos a ejecutar el asesor experto para la optimización automática de proyectos especificando la base de datos creada en el parámetro de entrada. Después de la primera ejecución, resulta que el asesor experto de optimización automática necesita mejoras, ya que no recibe información de la base de datos sobre la necesidad de utilizar el periodo forward. Tras realizar las adiciones, el código de la función de obtención de la siguiente tarea de optimización de la base de datos se verá así (las líneas añadidas están resaltadas):

//+------------------------------------------------------------------+
//| Get the next optimization task from the queue                    |
//+------------------------------------------------------------------+
ulong GetNextTask(string &setting) {
// Result
   ulong res = 0;

// Request to get the next optimization task from the queue
   string query = "SELECT s.expert,"
                  "       s.optimization,"
                  "       s.from_date,"
                  "       s.to_date,"
                  "       s.forward_mode,"
                  "       s.forward_date,"
                  "       j.symbol,"
                  "       j.period,"
                  "       j.tester_inputs,"
                  "       t.id_task,"
                  "       t.optimization_criterion"
                  "  FROM tasks t"
                  "       JOIN"
                  "       jobs j ON t.id_job = j.id_job"
                  "       JOIN"
                  "       stages s ON j.id_stage = s.id_stage"
                  " WHERE t.status IN ('Queued', 'Processing')"
                  " ORDER BY s.id_stage, j.id_job, t.status LIMIT 1;";

// Open the database
   if(DB::Connect()) {
      // Execute the request
      int request = DatabasePrepare(DB::Id(), query);

      // If there is no error
      if(request != INVALID_HANDLE) {
         // Data structure for reading a single string of a query result 
         struct Row {
            string   expert;
            int      optimization;
            string   from_date;
            string   to_date;
            int      forward_mode;
            string   forward_date;
            string   symbol;
            string   period;
            string   tester_inputs;
            ulong    id_task;
            int      optimization_criterion;
         } row;

         // Read data from the first result string
         if(DatabaseReadBind(request, row)) {
            setting =  StringFormat(
                          "[Tester]\r\n"
                          "Expert=%s\r\n"
                          "Symbol=%s\r\n"
                          "Period=%s\r\n"
                          "Optimization=%d\r\n"
                          "Model=1\r\n"
                          "FromDate=%s\r\n"
                          "ToDate=%s\r\n"
                          "ForwardMode=%d\r\n"
                          "ForwardDate=%s\r\n"
                          "Deposit=10000\r\n"
                          "Currency=USD\r\n"
                          "ProfitInPips=0\r\n"
                          "Leverage=200\r\n"
                          "ExecutionMode=0\r\n"
                          "OptimizationCriterion=%d\r\n"
                          "[TesterInputs]\r\n"
                          "idTask_=%d\r\n"
                          "fileName_=%s\r\n"
                          "%s\r\n",
                          GetProgramPath(row.expert),
                          row.symbol,
                          row.period,
                          row.optimization,
                          row.from_date,
                          row.to_date,
                          row.forward_mode,
                          row.forward_date,
                          row.optimization_criterion,
                          row.id_task,
                          fileName_,
                          row.tester_inputs
                       );
            res = row.id_task;
         } else {
            // Report an error if necessary
            PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d",
                        query, GetLastError());
         }
      } else {
         // Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
      }

      // Close the database
      DB::Close();
   }

   return res;
}


También hemos añadido una función para obtener la ruta del archivo del asesor experto optimizado desde la carpeta actual relativa a la carpeta raíz de los asesores expertos del terminal:

//+------------------------------------------------------------------+
//| Getting the path to the file of the optimized EA from the current|
//| folders relative to the root folder of terminal EAs              |
//+------------------------------------------------------------------+
string GetProgramPath(string name) {
   string path = MQLInfoString(MQL_PROGRAM_PATH);
   string programName = MQLInfoString(MQL_PROGRAM_NAME) + ".ex5";
   string terminalPath = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Experts\\";
   path =  StringSubstr(path,
                        StringLen(terminalPath),
                        StringLen(path) - (StringLen(terminalPath) + StringLen(programName)));
   return path + name;
}

Esto ha permitido a la base de datos especificar solo el nombre de archivo del asesor experto optimizado en la tabla de etapas sin enumerar los nombres de las carpetas en las que se adjunta en relación con la carpeta raíz de asesores expertos\MQL5\Expertos\.

Las siguientes ejecuciones del asesor automatizado de optimización de proyectos han mostrado que los resultados de las pasadas forward se añaden correctamente a la tabla de pasadas junto con los resultados de las pasadas normales. Sin embargo, una vez terminada la etapa, resulta difícil distinguir qué pasadas pertenecen a cada periodo (principal o forward). Obviamente, podemos aprovechar el hecho de que las pasadas de periodos anteriores siempre vienen después de las pasadas normales, pero esto deja de funcionar si los resultados de varios problemas de optimización de periodos anteriores aparecen en la tabla passes. Por lo tanto, vamos a añadir una columna is_forward a la tabla passes para distinguir las pasadas normales de las pasadas forward. Y para facilitar la distinción entre pasadas normales y pasadas realizadas en el marco de la optimización, añadiremos al mismo tiempo la columna is_optimzation.

De paso, hemos descubierto una imprecisión: al formar la cadena de consulta SQL para insertar los datos con los resultados de una pasada, hemos sustituido el número de pasada como un entero con señal utilizando el especificador %d. Sin embargo, el número de pasada será un entero largo sin señal, por lo que deberá utilizarse el especificador %I64u para sustituir correctamente su valor en la cadena.

Luego añadiremos el valor de la función correspondiente de definición de la señal del periodo forward al código de formación de la consulta SQL para incorporar los datos de la pasada:

string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) {
   return StringFormat("INSERT INTO passes "
                       "VALUES (NULL, %d, %I64u, %d, %s,\n'%s',\n'%s') RETURNING rowid;",
                       s_idTask, pass, (int) MQLInfoInteger(MQL_FORWARD), values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}

Sin embargo, resulta que no funciona tan bien como nos hubiera gustado. La cuestión es que esta función se llama desde el asesor experto que se ejecuta en el terminal principal en el modo de recogida de frames de datos. Por ello, el resultado de la llamada MQLInfoInteger(MQL_FORWARD) para ella siempre retornará false.

Por consiguiente, la señal del periodo forward deberá obtenerse en el código que se ejecuta en los agentes de prueba y no en el terminal principal del gráfico, es decir, en el manejador del evento de finalización de la pasada de prueba. Junto a ella, también hemos añadido una señal de optimización. 

//+------------------------------------------------------------------+
//| Handling completion of tester pass for agent                     |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Custom criteria
                            string params    // Description of EA parameters in the current pass
                           ) {
   ...

// Generate a string with pass data
   data = StringFormat("%d, %d, %s,'%s'", 
                       MQLInfoInteger(MQL_OPTIMIZATION), 
                       MQLInfoInteger(MQL_FORWARD),
                       data, params);

   ...
}


Tras realizar estas modificaciones y reiniciar el asesor de autooptimización, por fin vemos la imagen deseada en la tabla de pasadas:


Fig. 1. Tabla passes tras la ejecución de la tarea de optimización con periodo forward


Como podemos ver, la tarea de optimización con id_task = 1 ha tenido 40 pasadas. De ellas, 20 han sido normales (las 20 primeras líneas con is_forward = 0) y las 20 restantes han sido pasadas de periodo forward (is_forward = 1). El número de pasadas del simulador en la columna passes toma valores de 1 a 20 y cada uno ocurre exactamente 2 veces (una vez para el periodo principal, la segunda vez, para el periodo forward).


Preparando el inicio de la optimización completa

Una vez hemos comprobado que la base de datos recibe ahora correctamente los resultados de las pasadas realizadas utilizando el periodo forward, realizaremos una prueba más realista de la optimización automática. Para ello, añadiremos ya dos etapas a la base de datos limpia. La primera optimizará una única instancia de estrategia comercial, pero solo en un símbolo y un periodo (EURGBP H1) en el marco temporal 2018 - 2023. En esta fase no se usará el periodo forward. La segunda etapa optimizará un grupo de buenas instancias individuales obtenidas en la primera etapa. Ahora ya se usará el periodo forward: se dedicará el año 2023 por completo.

Fig. 2. Tabla de etapas stages con dos etapas


Para cada etapa en la tabla jobs, crearemos los trabajos que se realizarán dentro de esa etapa. En esta tabla, además del símbolo y el periodo, se especificarán los parámetros de entrada para los asesores expertos optimizados con el rango y el paso de cambio.

Fig. 3. Tabla jobs con dos trabajos para la primera y segunda etapa, respectivamente


Para el primer trabajo (id_job = 1), crearemos varias tareas de optimización que se diferenciarán en el valor del criterio de optimización (optimization_criterion = 0 ... 7) . Iteraremos todos los criterios uno por uno, y utilizaremos el criterio complejo dos veces: al principio y al final del primer trabajo (optimization_criterion = 7). Para la tarea realizada en el segundo trabajo (id_job = 2), utilizaremos un criterio de optimización personalizado (optimization_criterion = 6)


Fig. 4. Tabla de tareas tasks con las tareas para el primer y el segundo trabajo


Ahora ejecutaremos el asesor experto de optimización automática en cualquier gráfico del terminal y esperaremos a que finalicen todas las tareas. Con los agentes disponibles, el proceso ha durado en total unas 4 horas.


Análisis preliminar de los resultados

En la anterior optimización automática, solo teníamos un problema de optimización que utilizaba un periodo forward. Como criterio de optimización en ella actuaba nuestro criterio personalizado que calculaba el beneficio medio anual normalizado para una pasada determinada. Echemos un vistazo ahora a la nube de puntos con los valores de este criterio en el periodo principal. 

Fig. 5. Nube de puntos con los valores del beneficio medio anual normalizado para diferentes pasadas del periodo principal


El gráfico muestra que el valor de nuestro criterio oscila entre 1 000$ y 8 000$. Los puntos rojos correspondientes con un valor 0 aparecen porque algunas combinaciones de índices de instancia única en los parámetros de entrada tienen valores repetidos. Estos parámetros de entrada se consideran grupos de estrategias incorrectos, por lo que no tendremos resultados de estas pasadas. Podemos observar una tendencia general a que el beneficio medio anual normalizado aumente en las últimas pasadas. Por término medio, los mejores resultados obtenidos duplican aproximadamente los de las primeras pasadas, en las que los parámetros se seleccionan casi al azar.

Veamos ahora la nube de puntos con los resultados de las pasadas del periodo anterior. Habrá menos (unos 13 000 en lugar de 17 000) debido a las combinaciones de parámetros eliminadas en la fase principal que se consideraron incorrectas.

Fig. 6. Nube de puntos con los valores de beneficio medio anual normalizado para diferentes pasadas sobre el periodo forward


Aquí, la imagen de la disposición de los puntos ya es distinta. No se observa un aumento pronunciado de los resultados obtenidos al incrementar el número de pasadas. Por el contrario, podemos ver que al aumentar el número de pasadas, los resultados alcanzan primero valores más altos que al principio, y luego la tendencia se invertirá. A medida que aumenta el número de pasadas, sus resultados empiezan a disminuir por término medio, y la tasa de disminución aumenta a medida que nos acercamos al límite derecho de los números.

No obstante, resulta que esta imagen no siempre será así. Con otros ajustes en los rangos de los parámetros que deben buscarse durante la optimización, las nubes de puntos para las pasadas en los periodos principal y forward pueden tener este aspecto:

Fig. 7. Nube de puntos con valores del beneficio medio anual normalizado en los periodos básico y forward con otras configuraciones de optimización


Como podemos ver, la imagen es más o menos la misma en el periodo principal, solo que el rango del criterio es ahora un poco diferente: de 1 500$ a 5 000$. Sin embargo, en el periodo forward, la naturaleza de la nube es muy diferente. Los valores máximos no se alcanzan en las pasadas que se encuentran hacia la mitad del proceso de optimización, sino solo cerca del final. Además, por término medio, los valores del criterio en el periodo anterior son menores en un factor de aproximadamente 10 en lugar de 3 veces como en el primer proceso de optimización.

La intuición nos decía que, para aumentar la estabilidad de los resultados obtenidos a lo largo de distintos periodos, debíamos elegir un grupo que tuviera aproximadamente los mismos resultados en los periodos principal y forward. Sin embargo, los resultados obtenidos nos hicieron dudar mucho de que pudiéramos conseguir algo útil de esta manera. Especialmente cuando incluso el valor máximo del criterio en el periodo forward es notablemente menor que valores no especialmente buenos del criterio en el periodo principal. No obstante, vamos a intentarlo. Buscaremos pasadas convencionalmente "cercanas" en los periodos base y forward y observaremos sus resultados en el periodo base, forward y 2024.


Selección de pasadas

Recordemos cómo elegimos el mejor grupo basándonos en los resultados en el periodo forward en la parte 7. He aquí un resumen del algoritmo con algunos pequeños retoques:

  • Ahora ajustaremos el valor del beneficio medio anual normalizado para las pasadas forward tomando la reducción máxima de dos valores para el cálculo: en el periodo principal y en el periodo forward. Obtendremos el valor OOS_ForwardResultCorrected.

  • En la tabla combinada de resultados de optimización para 2018-2022 (periodo principal) y para 2023 (periodo forward), calcularemos para todos los indicadores la relación entre sus valores en los periodos principal y forward. 
    Por ejemplo, para el número de transacciones: TradesRatio = OOS_Trades / IS_Trades, y para el beneficio medio anual normalizado: ResultRatio = OOS_ForwardResultCorrected / IS_BackResult.
    Cuanto más cercanas a 1 sean estas relaciones, más similares serán los valores de estos indicadores en los dos periodos.

  • Luego calcularemos para todas estas relaciones la suma de sus desviaciones respecto a la unidad. Este valor supondrá nuestra medida de la diferencia entre los resultados de cada grupo en los periodos principal y forward:


    SumDiff = |1 - ResultRatio| + ... + |1 - TradesRatio|.

  • Debemos tener en cuenta que en el periodo principal y en el forward para cada pasada la reducción podría ser diferente. Después elegiremos la reducción máxima de dos periodos y la utilizaremos para calcular el factor de escala de las posiciones abiertas para lograr una reducción normalizada del 10%:

    Scale = 10 / MAX(OOS_EquityDD, IS_EquityDD).

  • Ahora elegiremos preferentemente conjuntos en los que SumDiff sea menor y Scale sea mayor. Para ello, calcularemos el último indicador:

    Res = Scale / SumDiff.

  • Vamos a clasificar todos los grupos según el valor Res calculado en el paso anterior en orden descendente. A continuación, en la parte superior de la tabla se encontrarán aquellos grupos cuyos resultados en los periodos principal y forward hayan sido más semejantes, siendo además menor la reducción en ambos periodos.

Además, sugerimos repetir la selección de grupos varias veces, eliminando aquellos que contengan el número de instancias únicas de estrategias comerciales ya incluidas en los grupos. Pero este paso resultará relevante cuando se agrupen instancias individuales de antemano, de modo que índices diferentes se correspondan con instancias disímiles en los resultados. Como aún no hemos llegado a la clusterización en la optimización automática, nos saltaremos este paso.

En su lugar, podemos añadir un segundo nivel de agrupación según diferentes marcos temporales para cada símbolo y un tercer nivel según diferentes símbolos.

Vamos a mejorar un poco el algoritmo anterior. Para empezar, lo que queremos saber es a qué distancia se hallan dos conjuntos de resultados de una pasada en un espacio con una dimensionalidad igual al número de resultados (características) comparados. Para ello, utilizaremos una norma de primer orden con algún factor de escala para hallar la distancia de un punto con las coordenadas de las relaciones de los resultados comparados hasta un punto fijo con coordenadas unitarias. No obstante, entre estas relaciones puede haber cercanas a 1 o muy distantes. Y esto último puede empeorar la estimación global de la distancia. Por lo tanto, intentaremos sustituir la variante propuesta anteriormente calculando la distancia euclidiana habitual entre dos vectores de resultados a los que aplicaremos primero el escalado min-max.

Al final tendremos que escribir una consulta SQL bastante compleja (aunque puede haber consultas mucho más complicadas). Ahora veremos con más detalle el proceso de creación de la consulta necesaria. Empezaremos con consultas sencillas y progresivamente las iremos haciendo más complicadas. Pondremos algunos de los resultados en tablas temporales que se usarán en consultas posteriores. Después de cada consulta, le mostraremos cómo son los resultados.

Así, los datos de origen de los que necesitamos sacar algo estarán básicamente en la tabla passes. Ahora no aseguraremos de que realmente estén ahí, y haremos una selección inmediata solo de aquellas pasadas que se han realizado como parte de la tarea de optimización deseada. En nuestro caso particular, el identificador id_task correspondiente a la segunda etapa de optimización para EURGBP H1 tenía un valor de 10. Por consiguiente, lo utilizaremos en el texto de la consulta:

-- Request 1

SELECT *
  FROM passes p0
 WHERE p0.id_task = 10;



Podemos ver que en la tabla passes hay más de 22 mil registros para esta tarea con id_task=10.

El siguiente paso consistirá en combinar en una sola línea los resultados de las dos líneas de este conjunto de datos que correspondan al mismo número de pasadas del simulador, pero a periodos distintos: el periodo principal y el periodo forward. Limitaremos temporalmente el número de columnas mostradas en el resultado, dejando solo aquellas por las que podemos comprobar la corrección de la selección de las cadenas. Nombraremos las columnas resultantes según la siguiente regla: al nombre de la columna le añadiremos el prefijo "I_" para el periodo principal (In-Sample) y el prefijo "O_" para el periodo forward (Out-Of-Sample):

-- Request 2

SELECT p0.id_pass AS I_id_pass,
       p0.is_forward AS I_is_forward,
       p0.custom_ontester AS I_custom_ontester,
       p1.id_pass AS O_id_pass,
       p1.is_forward AS O_is_forward,
       p1.custom_ontester AS O_custom_ontester
  FROM passes p0
       JOIN
       passes p1 ON p0.pass = p1.pass AND 
                    p0.is_forward = 0 AND 
                    p1.is_forward = 1
 WHERE p0.id_task = 10 AND p1.id_task = 10


Como resultado, el número de filas ha disminuido exactamente dos veces, es decir, por cada pasada sobre el periodo principal en la tabla passes ha habido exactamente una pasada sobre el periodo forward y viceversa.

Ahora volveremos a la primera consulta para realizar la normalización. Si dejamos la normalización para más adelante, cuando ya tengamos columnas separadas para el mismo parámetro en los periodos principal y forward, será más difícil calcular los valores mínimo y máximo de ambos a la vez. En primer lugar, elegiremos un pequeño número de parámetros con los que estimaremos la "distancia" entre los resultados del periodo principal y del periodo forward. Por ejemplo, practicaremos primero el cálculo de la distancia para tres parámetros: custom_ontester, equity_dd_relative, profit_factor.

Tendremos que convertir las columnas con los valores de estos parámetros en columnas con valores que vayan de 0 a 1. Utilizaremos las funciones de ventana para obtener los valores mínimo y máximo de las columnas de la consulta. Para los nombres de columna escalados, añadiremos el prefijo "s_" a los nombres de columna originales.  Basándonos en los resultados retornados por esta consulta, crearemos y rellenaremos una nueva tabla utilizando el comando

CREATE TABLE ... AS SELECT ... ;

Y veremos el contenido de la nueva tabla creada y rellenada:

-- Request 3

DROP TABLE IF EXISTS t0;

CREATE TABLE t0 AS
SELECT id_pass,
       pass,
       is_forward,
       custom_ontester,
       (custom_ontester - MIN(custom_ontester) OVER () ) / (MAX(custom_ontester) OVER () - MIN(custom_ontester) OVER () ) AS s_custom_ontester,
       equity_dd_relative,
       (equity_dd_relative - MIN(equity_dd_relative) OVER () ) / (MAX(equity_dd_relative) OVER () - MIN(equity_dd_relative) OVER () ) AS s_equity_dd_relative,
       profit_factor,
       (profit_factor - MIN(profit_factor) OVER () ) / (MAX(profit_factor) OVER () - MIN(profit_factor) OVER () ) AS s_profit_factor
  FROM passes

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

WHERE id_task=10;


Como podemos ver, junto a cada parámetro evaluado aparecía una nueva columna con el valor de este parámetro, reducido al intervalo de 0 a 1.

Ahora reformaremos el texto de la segunda consulta para que tome los datos de la nueva tabla t0 en lugar de passes y ponga los resultados en la nueva tabla t1. Tomaremos los valores ya escalados y los redondearemos por comodidad. También dejaremos solo las filas en las que los valores del beneficio normalizado en los periodos principal y forward sean positivos:

SELECT * FROM t0;


El número de filas se ha reducido aproximadamente un tercio en comparación con la segunda consulta, pero ahora solo nos quedan las pasadas en las que tanto el periodo principal como el forward han obtenido beneficios.

Finalmente, hemos llegado al último paso del proceso de desarrollo de consultas. Lo único que queda por hacer es calcular la distancia entre las combinaciones de parámetros para los periodos principal y forward en cada fila de la tabla t1 y clasificarlas en orden ascendente de distancia:

-- Request 4

DROP TABLE IF EXISTS t1;

CREATE TABLE t1 AS 
SELECT p0.id_pass AS I_id_pass,
       p0.is_forward AS I_is_forward,
       ROUND(p0.s_custom_ontester, 4) AS I_custom_ontester,
       ROUND(p0.s_equity_dd_relative, 4) AS I_equity_dd_relative,
       ROUND(p0.s_profit_factor, 4) AS I_profit_factor,
       p1.id_pass AS O_id_pass,
       p1.is_forward AS O_is_forward,
       ROUND(p1.s_custom_ontester, 4) AS O_custom_ontester,
       ROUND(p1.s_equity_dd_relative, 4) AS O_equity_dd_relative,
       ROUND(p1.s_profit_factor, 4) AS O_profit_factor
  FROM t0 p0
       JOIN
       t0 p1 ON p0.pass = p1.pass AND 
                    p0.is_forward = 0 AND 
                    p1.is_forward = 1 AND
                    p0.custom_ontester > 0 AND
                    p1.custom_ontester > 0;

SELECT * FROM t1;


El identificador de pasada I_id_pass de la fila superior de los resultados obtenidos corresponderá a la pasada con la menor distancia entre los valores de los resultados en el periodo principal y en el periodo forward.

Vamos a tomar esta y el identificador de la mejor pasada de beneficio normalizado en el periodo principal. No coinciden, así que vamos a convertirlas en una biblioteca de parámetros para el EA final, como se describe en el último artículo. Hemos tenido que hacer pequeñas modificaciones en los archivos añadidos en el último artículo para asegurarnos de poder especificar una base de datos concreta al crear y exportar la biblioteca de conjuntos de parámetros.


Resultados

Así que tendremos dos opciones de configuración en la biblioteca. La primera variante se denominará "Best for dist(IS, OS) (2018-2023)" , y será la mejor pasada de optimización con la menor distancia entre los valores de los parámetros. La segunda opción se denominará "Best on IS (2018-2022)", y será la mejor pasada de optimización sobre el beneficio normalizado para el periodo básico de 2018 a 2022.

Fig. 8. Selección de un grupo de ajustes de la biblioteca en el EA final


Veamos los resultados de estos dos grupos para el periodo 2018-2023, que ha participado al completo en la optimización.

Fig. 9. Resultados del primer grupo (mejores según la distancia) para el periodo 2018-2023


Fig. 10. Resultados del segundo grupo (mejor beneficio) para el periodo 2018-2023


Podemos ver que ambos grupos están bien normalizados durante este periodo temporal (la reducción máxima es de 1 000$ en ambos casos). Sin embargo, el primero tiene un beneficio medio anual de aproximadamente la mitad que el segundo (1 724$ frente a 3 430$). Aquí las ventajas del primer grupo aún no resultan evidentes. 

Veamos ahora los resultados de estos dos grupos en 2024 (hasta octubre), que no ha participado en la optimización.

Fig. 11. Resultados del primer grupo (mejor según la distancia) para el periodo 2024


Fig. 12. Resultados del segundo grupo (mejor según el beneficio) para el periodo 2024

Ambos resultados son negativos en este periodo, pero el segundo sigue pareciendo mejor que el primero. Debemos señalar que la reducción máxima para este periodo ha sido siempre inferior a 1 000$.

Pues bien, dado que 2024 no ha sido un año muy fructífero para este símbolo, veamos cuáles serán los resultados del periodo situado no después, sino antes del periodo de optimización. Tomaremos un periodo aún más largo, ya que tenemos esa opción (tres años de 2015 a 2017).

Fig. 13. Resultados del primer grupo (mejor según la distancia) en el periodo 2015-2017



Fig. 14. Resultados del segundo grupo (mejor según el beneficio) en el periodo 2015-2017


Durante este periodo, la reducción ya ha superado la reducción calculada admisible. Ha sido aproximadamente 1,5 veces mayor en la primera versión y unas 3,5 veces mayor en la segunda. En este sentido, la primera opción es ligeramente mejor, ya que el exceso de reducción resulta notablemente menor que en la segunda y, en general, no es muy grande. Además, la primera opción no tiene un notable hundimiento del gráfico en el centro, como la segunda. Es decir, la primera opción ha mostrado una mayor adaptabilidad a un periodo desconocido de la historia en comparación con la segunda opción. Sin embargo, en términos de ingresos medios anuales normalizados, la diferencia entre las dos opciones no es tan grande (857$ frente a 615$). Desgraciadamente, no podemos calcular este valor para un periodo desconocido de antemano.

Por consiguiente, en esta fase se seguirá favoreciendo la primera opción. Resumamos.


Conclusión

Bien, hoy hemos implementado la automatización de la segunda etapa de optimización utilizando el periodo forward. Tampoco esta vez se han identificado ventajas claras. La tarea ha resultado mucho más amplia y laboriosa de lo que habíamos previsto en un principio. En el proceso de trabajo han surgido muchas preguntas nuevas que aún esperan respuesta.

Hemos podido comprobar que si el periodo forward cae por suerte en un periodo fallido de trabajo del EA, parece que no podremos utilizarlo para seleccionar buenas combinaciones de parámetros.

Si la duración de las transacciones es larga, los resultados de una pasada con una interrupción en el límite de los periodos principal y forward pueden diferir notablemente de los resultados de una pasada continua. Esto también pone en duda la conveniencia de usar el periodo forward de esta manera, no el periodo forward en general, sino específicamente una forma de seleccionar automáticamente los parámetros que tienen más probabilidades de mostrar resultados comparables en el futuro.

Ahora hemos utilizado una forma sencilla de calcular la distancia entre los resultados de las pasadas. Es posible que si complicamos este método, los resultados mejoren. Además, aún no hemos escrito una implementación que permita seleccionar automáticamente la mejor pasada para incluir en un grupo de conjuntos para diferentes símbolos y marcos temporales. Casi todo está listo para ello: lo único que necesitamos hacer es llamar a las consultas SQL que hemos desarrollado desde el EA. Pero como aún es probable que las modifiquemos, pospondremos esta automatización para el futuro.

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.15683
1 Advisor.mqh 1.04. Clase básica del experto Parte 10
2 Database.mqh 1.05 Clase para trabajar con bases de datos Parte 18
3 database.sqlite.schema.sql
Esquema de la base de datos Parte 18
4 ExpertHistory.mqh 1.00 Clase para exportar la historia de transacciones a un archivo Parte 16
5 ExportedGroupsLibrary.mqh
Archivo generado con los nombres de los grupos de estrategias y un array con sus cadenas de inicialización Parte 17
6 Factorable.mqh 1.01 Clase básica de objetos creados a partir de una cadena Parte 10
7 GroupsLibrary.mqh 1.01 Clase para trabajar con una biblioteca de grupos estratégicos seleccionados Parte 18
8 HistoryReceiverExpert.mq5 1.00 Asesor experto para reproducir la historia de transacciones con el gestor de riesgos Parte 16  
9 HistoryStrategy.mqh  1.00 Clase de estrategia comercial para reproducir la historia de transacciones  Parte 16
10 Interface.mqh 1.00 Clase básica de visualización de diversos objetos Parte 4
11 LibraryExport.mq5 1.01 Asesor que guarda las cadenas de inicialización de las pasadas seleccionadas de la biblioteca en el archivo ExportedGroupsLibrary.mqh Parte 18
12 Macros.mqh 1.02 Macros útiles para operaciones con arrays Parte 16  
13 Money.mqh 1.01  Clase básica de gestión de capital Parte 12
14 NewBarEvent.mqh 1.00  Clase de definición de una nueva barra para un símbolo específico  Parte 8
15 Optimization.mq5  1.02 Asesor que gestiona el inicio de tareas de optimización Parte 18
16 Receiver.mqh 1.04.  Clase básica de transferencia de volúmenes abiertos a posiciones de mercado  Parte 12
17 SimpleHistoryReceiverExpert.mq5 1.00 Asesor experto simplificado para reproducir la historia de transacciones   Parte 16
18 SimpleVolumesExpert.mq5 1.20 Asesor experto para el trabajo en paralelo de varios grupos de estrategias modelo. Los parámetros se tomarán de la biblioteca de grupos incorporada. Parte 17
19 SimpleVolumesStage1.mq5
1.17 Asesor experto para optimizar una única instancia de una estrategia comercial (Etapa 1)  Parte 18
20 SimpleVolumesStage2.mq5
1.01 Asesor experto que optimiza un grupo de instancias de estrategias comerciales (Etapa 2)
Parte 18
21 SimpleVolumesStage3.mq5 1.01 Asesor experto que guarda un grupo normalizado generado de estrategias en una biblioteca de grupos con un nombre especificado. Parte 18
22 SimpleVolumesStrategy.mqh 1.09  Clase de estrategia comercial con uso de volúmenes de ticks Parte 15
23 Strategy.mqh 1.04.  Clase básica de estrategia comercial Parte 10
24 TesterHandler.mqh  1.04. Clase para gestionar los eventos de optimización  Parte 18  
25 VirtualAdvisor.mqh  1.07  Clase del asesor experto que trabaja con posiciones (órdenes) virtuales Parte 18
26 VirtualChartOrder.mqh  1.01  Clase de posición virtual gráfica Parte 18  
27 VirtualFactory.mqh 1.04.  Clase de fábrica de objetos  Parte 16
28 VirtualHistoryAdvisor.mqh 1.00  Clase experta para reproducir la historia de transacciones  Parte 16
29 VirtualInterface.mqh  1.00  Clase de GUI del asesor  Parte 4  
30 VirtualOrder.mqh 1.04.  Clase de órdenes y posiciones virtuales  Parte 8
31 VirtualReceiver.mqh 1.03  Clase de transferencia de volúmenes abiertos a posiciones de mercado (receptor)  Parte 12
32 VirtualRiskManager.mqh  1.02  Clase de gestión de riesgos (gestor de riesgos)  Parte 15
33 VirtualStrategy.mqh 1.05  Clase de estrategia comercial con posiciones virtuales  Parte 15
34 VirtualStrategyGroup.mqh  1.00  Clase de grupo o grupos de estrategias comerciales Parte 11 
35 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/15683

Archivos adjuntos |
MQL.zip (86.02 KB)
Desarrollo de un Asesor Experto (EA) en MQL5 basado en la estrategia de ruptura del rango de consolidación Desarrollo de un Asesor Experto (EA) en MQL5 basado en la estrategia de ruptura del rango de consolidación
Este artículo describe los pasos para crear un Asesor Experto (EA) que aproveche las rupturas de precios después de los períodos de consolidación. Al identificar rangos de consolidación y establecer niveles de ruptura, los operadores pueden automatizar sus decisiones comerciales basándose en esta estrategia. El Asesor Experto tiene como objetivo proporcionar puntos de entrada y salida claros y evitar rupturas falsas.
Redes neuronales: así de sencillo (Parte 95): Reducción del consumo de memoria en los modelos de transformadores Redes neuronales: así de sencillo (Parte 95): Reducción del consumo de memoria en los modelos de transformadores
Los modelos basados en la arquitectura de transformadores demuestran una gran eficacia, pero su uso se complica por el elevado coste de los recursos tanto en la fase de formación como durante el funcionamiento. En este artículo, propongo familiarizarse con los algoritmos que permiten reducir el uso de memoria de tales modelos.
Indicador personalizado: Trazado de puntos de entradas parciales en cuentas netting Indicador personalizado: Trazado de puntos de entradas parciales en cuentas netting
En este artículo, exploraremos una forma interesante y diferente de crear un indicador en MQL5. En lugar de centrarnos en una tendencia o patrón gráfico, el objetivo será gestionar nuestras propias posiciones, incluyendo las entradas y salidas parciales. Utilizaremos intensivamente matrices dinámicas y algunas funciones comerciales (Trade) relacionadas con el historial de transacciones y las posiciones abiertas para indicar en el gráfico dónde se llevaron a cabo estas operaciones.
Desarrollamos un asesor experto multidivisa (Parte 17): preparación adicional para el trading real Desarrollamos un asesor experto multidivisa (Parte 17): preparación adicional para el trading real
Ahora nuestro EA utiliza una base de datos para recuperar las cadenas de inicialización de instancias individuales de estrategias comerciales. Sin embargo, la base de datos es bastante voluminosa y contiene mucha información innecesaria para el funcionamiento real del asesor experto. Vamos a intentar que el EA funcione sin conexión obligatoria a la base de datos.