Cómo implementar sus propios criterios de optimización

Nikolai Shevchuk | 30 marzo, 2016

Introducción

De vez en cuando se oyen comentarios sobre la necesidad de ampliar los criterios de optimización incluidos en la prueba de estrategias de MT4. Como puede imaginar, sean los que sean los criterios que añadan los desarrolladores, siempre habrán usuarios y situaciones que requieran otros criterios. ¿Podemos solucionar esto en MQL4 y MetaTrader? Sí, podemos. En este artículo veremos la implementación de un criterio personalizado de optimización con el ejemplo del Asesor Experto estándar Moving Average (Promedio móvil). El criterio que vamos a considerar es la relación benefició/disminución.

El Asesor Experto

Vamos a empezar con el criterio de optimización. Para sus cálculos tenemos que hacer el seguimiento de los máximos del saldo y de la disminución durante la prueba. Para no depender de la lógica de trabajo del Asesor Experto, vamos a añadir estas líneas de código al principio de la función start().

   if (AccountEquity() > MaxEqu) MaxEqu = AccountEquity();
   if (MaxEqu-AccountEquity() > MaxDD) MaxDD = MaxEqu-AccountEquity();

Para procesar el último tick, hay que ponerlas en la función deinit(). Después de esto podemos calcular el valor del criterio de optimización.

    Criterion = (AccountBalance()-StartBalance)/MaxDD;

Ahora podemos empezar con la parte principal: el seguimiento del proceso de optimización. Tenemos un problema: MQL4 no dispone de instrumentos integrados para determinar el final de la optimización. La única solución que yo conozca es lo que se llama "optimización con un contador". Esto significa lo siguiente: el único parámetro modificable del Asesor Experto es la variable externa "counter" (contador). Todavía hay un inconveniente muy importante: ya no podemos cambiar los parámetros del Asesor Experto de forma convencional y tenemos que proporcionarlos nosotros mismos. Otra desventaja es el hecho de que nuestra amiga la caché de optimización se convierte en nuestra enemiga. Pero el fin justifica los medios, sigamos.

Vamos a añadir las variables externas:

extern int Counter                    = 1;    // Counter of the tester's running
extern int TestsNumber                = 200;  // he check digit - total number of runs 
extern int MovingPeriodStepsNumber    = 20;   // Number of optimization steps for MovingPeriod 
extern int MovingShiftStepsNumber     = 10;   // Number of optimization steps for MovingShift
extern double MovingPeriodLow         = 150;  // Lower limit of the optimization range for MovingPeriod
extern double MovingShiftLow          = 1;    // Lower limit of the optimization range for MovingShift
extern double MovingPeriodStep        = 1;    // Optimization step for MovingPeriod 
extern double MovingShiftStep         = 1;    // Optimization step for MovingShift

La primera variable es el contador. La siguiente es el control (y la información). Después asignamos el número de pasos, el límite inferior y el paso de optimización de las dos variables del Promedio móvil destinadas a la optimización. Puede observar algunas redundancias: si vamos a llevar a cabo un análisis exhaustivo (y lo vamos a hacer) el producto de MovingPeriodStepsNumber por MovingShiftStepsNumber debe ser igual a TestsNumber.

Después de cada ejecución de la prueba, el Asesor Experto termina por completo su trabajo y se puede considerar la siguiente ejecución como su reencarnación. Podemos gestionar "el almacenamiento genético" de dos maneras: unas variables globales y un archivo independiente. Usaremos ambas.

Vamos a modificar la función init():

int init() {
  if (IsTesting() && TestsNumber > 0) {
    if (GlobalVariableCheck("FilePtr")==false || Counter == 1) {
      FilePtr = 0; 
      GlobalVariableSet("FilePtr",0); 
    } else {
      FilePtr = GlobalVariableGet("FilePtr"); 
    }
    MovingPeriod = MovingPeriodLow+((Counter-1)/MovingShiftStepsNumber)*MovingPeriodStep;
    MovingShift = MovingShiftLow+((Counter-1)%MovingShiftStepsNumber)*MovingShiftStep;
    StartBalance = AccountBalance();
    MaxEqu = 0;
    MaxDD = 0;
  }   
  return(0);
}

Lo que hemos añadido se encuentra dentro de las condiciones de funcionamiento en la prueba y en el TestsNumber distinto de cero. De modo que TestsNumber=0 hará que el Asesor Experto funcione como un Promedio móvil estándar. Mientras hablamos del proceso de optimización, tenemos que aprovechar cualquier oportunidad para acelerar el proceso. Por esta razón, el código comienza proporcionando la gestión a través del puntero del archivo (a través de las ejecuciones de la prueba) usando una variable global. Después calculamos los valores de los parámetros modificables y inicializamos las variables necesarias para el cálculo del criterio de optimización.

La mayor parte del trabajo se lleva a cabo en la función deinit(). Se guardarán los resultados de la prueba en un archivo de texto con los valores del criterio de optimización, los valores de los parámetros optimizados y el número de ejecuciones de la prueba. Después de finalizar la optimización, se ordenan los resultados en función del criterio de optimización y se guardan en el mismo archivo. De modo que tenemos que procesar tres situaciones: el primer inicio, el último inicio y todos los demás. Vamos a utilizar el contador de ejecuciones de la prueba para separar estos inicios.
Procesando el primer inicio:

    if (Counter == 1) {
// First run, create/initialize a datafile.
      h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,';');
      FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
// Remember the position of the file pointer after writing in the global variable
      FilePtr = FileTell(h); 
      GlobalVariableSet("FilePtr",FilePtr);
      FileClose(h);

La peculiaridad de procesar otros inicios es que se añaden datos nuevos al archivo:

    } else {
//  After the first start is processed, the data are added into the file
      h=FileOpen("test.txt",FILE_CSV|FILE_READ|FILE_WRITE,';');
//  It is time to use the file pointer written in the global variable
      FilePtr = GlobalVariableGet("FilePtr");
      FileSeek(h,FilePtr, SEEK_SET);
      FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
//  Remember the file pointer position once again
      FilePtr = FileTell(h); 
      GlobalVariableSet("FilePtr",FilePtr);

Ahora vamos a procesar el último inicio:

      if (Counter == TestsNumber) {
        ArrayResize(Data,TestsNumber); 
// Returns the file pointer to the beginning       
        FileSeek(h,0,SEEK_SET);
// Read from the file the results of all testings
        int i = 0;
        while (i<TestsNumber && FileIsEnding(h)== false) {
          for (int j=0;j<4;j++) {
            Data[i][j]=FileReadNumber(h); 
          }
          i++;
        } 
// Sort the array according to our optimization criterion
        ArraySort(Data,WHOLE_ARRAY,0,MODE_DESCEND);
// Now let us arrange the results. Reopen the file        
        FileClose(h); 
        h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,' ');
        FileWrite(h,"  Criterion","     MovingPeriod"," MovingShift"," Counter");
        for (i=0;i<TestsNumber;i++) {
          FileWrite(h,DoubleToStr(Data[i][0],10),"        ",Data[i][1],"        ",Data[i][2],"        ",Data[i][3]);
        }

Se declaró al principio la matriz de tipo double Data[][4]. Esto es todo. Hagamos una limpieza:

        GlobalVariableDel("FilePtr");
      }
      FileClose(h); 
    }
  }

Compilamos, abrimos la prueba de estrategias y seleccionamos el Asesor Experto. Después abrimos las propiedades del Asesor Experto y comprobamos cuatro puntos:

- El producto de MovingPeriodStepsNumber por MovingShiftStepsNumber DEBE ser igual a TestsNumber.
- Se debe hacer la optimización ÚNICAMENTE para Counter,
- El rango de la optimización DEBE estar entre 1 y TestsNumber con un paso de 1.
- Hay que deshabilitar el algoritmo genético.

Iniciamos la optimización. Al finalizarse, nos dirigimos a la carpeta [Meta Trader]\tester\files y vemos los resultados en el archivo test.txt. El autor lo hizo para EURUSD_H1 a partir de mediados de 2004 en los precios de apertura y obtuvo los siguientes resultados:

Volvamos ahora a la cuestión de que la caché es nuestra enemiga. Lo que pasa en realidad es que al tomar los resultados de las pruebas desde la memoria caché no se inician las funciones init() y deinit(). Como resultado, no se contabiliza la totalidad o parte de las opciones al reiniciar la optimización. Además, puesto que el número real de ejecuciones será inferior a TestsNumber, habrá algunos ceros en la matriz Data. El autor sugiere dos formas de eliminar el "efecto de la memoria caché": compilar de nuevo el Asesor Experto o cerrar/pausar/abrir la ventana de la prueba de estrategias.
Se puede detectar el efecto de la memoria caché mediante un recuento por separado de las ejecuciones de las prueba. Hay tres inserciones para implementar este recuento mediante una variable global especial, propuesta en el Asesor Experto adjunto:

// Code of the independent counter
    if (GlobalVariableCheck("TestsCnt")==false || Counter == 1) {
      TestsCnt = 0; 
      GlobalVariableSet("TestsCnt",0); 
    } else {
      TestsCnt = GlobalVariableGet("TestsCnt"); 
    }
// Code of the independent counter
    TestsCnt++;
    GlobalVariableSet("TestsCnt",TestsCnt); 
// Code of the independent counter
        GlobalVariableDel("TestsCnt");

Una última cosa: Un lector atento se habrá dado cuenta de que podemos prescindir de la variable FilePtr (y la variable global que la acompaña); se escriben los datos al final del archivo y se leen desde el principio. Entonces, ¿para qué sirve? Esta es la respuesta: este Asesor Experto está diseñado para la demostración del método de seguimiento de la optimización. El método permite gestionar el trabajo "sobre la marcha" con los resultados de las pruebas anteriores. El puntero del archivo puede resultar extremadamente útil aquí, así como el contador independiente. Como ejemplos de las tareas que requieren trabajar "sobre la marcha" con los resultados anteriores podemos mencionar la gestión de las pruebas fuera de la muestra y la implementación de su propio algoritmo genético.

Conclusión

El motivo de tanto interés en este problema era un tema del foro http://forum.mql4.com.