Evento OnTester

El evento OnTester se genera al finalizar la simulación del Asesor Experto en datos históricos (tanto una ejecución de probador separada iniciada por el usuario como una de las múltiples ejecuciones lanzadas automáticamente por el probador durante la optimización). Para manejar el evento OnTester, un programa MQL debe tener una función correspondiente en su código fuente, pero esto no es necesario. Incluso sin la función OnTester, los Asesores Expertos pueden optimizarse con éxito basándose en criterios estándar.

La función sólo puede utilizarse en Asesores Expertos.

double OnTester()

La función está diseñada para calcular algún valor de tipo double, utilizado como criterio de optimización personalizado (Custom max). La selección de criterios es importante sobre todo para el éxito de la optimización genética, al tiempo que permite también al usuario evaluar y comparar los efectos de diferentes ajustes.

En la optimización genética, los resultados se ordenan en una generación según el criterio descendente. Es decir, los resultados con el valor más alto se consideran los mejores desde el punto de vista del criterio de optimización. Los peores valores de esta clasificación se descartan posteriormente y no participan en la formación de la siguiente generación.

Tenga en cuenta que los valores devueltos por la función OnTester sólo se tienen en cuenta cuando se selecciona un criterio personalizado en la configuración del probador. La disponibilidad de la función OnTester no significa automáticamente su utilización por el algoritmo genético.
 
La API de MQL5 no proporciona los medios para averiguar mediante programación qué criterio de optimización ha seleccionado el usuario en la configuración del probador. A veces es muy importante saberlo para poder aplicar algoritmos analíticos propios para postprocesar los resultados de la optimización.

La función es llamada por el núcleo sólo en el probador, justo antes de la llamada de la función OnDeinit.

Para calcular el valor de retorno, podemos utilizar tanto las estadísticas estándar disponibles a través de la función TesterStatistics como sus cálculos arbitrarios.

En el Asesor Experto BandOsMA.mq5, creamos el manejador OnTester que tiene en cuenta varias métricas: el beneficio, la rentabilidad, el número de operaciones y el ratio de Sharpe. A continuación, multiplicamos todas las métricas tras sacar la raíz cuadrada de cada una. Por supuesto, cada desarrollador puede tener sus propias preferencias e ideas para construir esos criterios de calidad generalizados.

double sign(const double x)
{
   return x > 0 ? +1 : (x < 0 ? -1 : 0);
}
   
double OnTester()
{
   const double profit = TesterStatistics(STAT_PROFIT);
   return sign(profit) * sqrt(fabs(profit))
      * sqrt(TesterStatistics(STAT_PROFIT_FACTOR))
      * sqrt(TesterStatistics(STAT_TRADES))
      * sqrt(fabs(TesterStatistics(STAT_SHARPE_RATIO)));
}

El registro de la prueba unitaria muestra una línea con el valor de la función OnTester.

Vamos a lanzar la optimización genética del Asesor Experto para 2021 en EURUSD, H1 con la selección de los parámetros del indicador y el tamaño de Stop Loss (el archivo MQL5/Presets/MQL5Book/BandOsMA.set se proporciona con el libro). Para comprobar la calidad de la optimización incluiremos también pruebas forward desde principios de 2022 (5 meses).

Primero, optimicemos según nuestro criterio.

Como usted sabe, MetaTrader 5 guarda todos los criterios estándar en los resultados de la optimización, además de la actual utilizada durante la optimización. Esto permite, una vez finalizada la optimización, analizar los resultados desde distintos puntos seleccionando determinados criterios en la lista desplegable de la esquina superior derecha del panel con la tabla. Así pues, aunque realizamos la optimización según nuestro propio criterio, también disponemos del criterio complejo integrado más interesante.

Podemos exportar la tabla de optimización a un archivo XML, primero con nuestros criterios seleccionados, y después con un criterio complejo dando un nuevo nombre al archivo (desafortunadamente, sólo se escribe un criterio en el archivo de exportación; es importante no cambiar la clasificación entre dos exportaciones). Esto permite combinar dos tablas en un programa externo y construir un diagrama en el que se trazan dos criterios a lo largo de los ejes; cada punto indica una combinación de criterios en una ejecución.

Comparación de criterios de optimización personalizados y complejos

Comparación de criterios de optimización personalizados y complejos

En un criterio complejo, observamos una estructura multinivel, ya que se calcula según una fórmula con condiciones: en algún lugar funciona una rama y en otro lugar funciona otra. Nuestros criterios personalizados se calculan siempre con la misma fórmula. También observamos la presencia de valores negativos en nuestro criterio (esto es de esperar) y el rango declarado de 0-100 para el criterio complejo.

Vamos a comprobar lo bueno que es nuestro criterio analizando sus valores para el periodo forward.

Valores del criterio personalizado en periodos de optimización y pruebas forward

Valores del criterio personalizado en periodos de optimización y pruebas forward

Como era de esperar, sólo una parte de los buenos indicadores de optimización se mantuvieron en forward. Sin embargo, estamos más interesados, no en el criterio, sino en el beneficio. Veamos su distribución en el enlace optimización-forward.

Beneficio en periodos de optimización y pruebas forward

Beneficio en periodos de optimización y pruebas forward

El panorama aquí es similar. De los 6850 pases con beneficio en el periodo de optimización, 3123 resultaron ser rentables también a plazo (45 %). Y de las 1000 mejores, sólo 323 fueron rentables, lo que no es suficiente. Por lo tanto, este Asesor Experto necesitará mucho trabajo para identificar configuraciones rentables estables. ¿Pero tal vez sea el problema de los criterios de optimización?

Repitamos la optimización, esta vez utilizando el criterio complejo integrado.

¡Atención! MetaTrader 5 genera cachés de optimización durante las optimizaciones: archivos opt en Tester/cache. Al iniciar la siguiente optimización, busca cachés adecuados para continuar la optimización. Si existe un archivo de caché con la configuración anterior, el proceso no empieza desde el principio, sino que tiene en cuenta los resultados anteriores. Esto le permite construir optimizaciones genéticas en cadena, asumiendo que encuentra los mejores resultados (al fin y al cabo, cada optimización genética es un proceso aleatorio).
 
MetaTrader 5 no tiene en cuenta el criterio de optimización como factor distintivo en la configuración. Esto puede ser útil en algunos casos, basándonos en lo anterior, pero interferirá con nuestra tarea actual. Para llevar a cabo un experimento puro, necesitamos una optimización desde cero. Por lo tanto, inmediatamente después de la primera optimización utilizando nuestro criterio, no podemos lanzar la segunda utilizando el criterio complejo.
 
No hay forma de desactivar el comportamiento actual desde la interfaz del terminal. Por lo tanto, deberá eliminar o renombrar (cambiar la extensión) el archivo opt anterior manualmente en cualquier gestor de archivos. Un poco más adelante nos familiarizaremos con la directiva de preprocesador para el probador tester_no_cache, que se puede especificar en el código fuente de un Asesor Experto en particular, lo que le permite desactivar la lectura de la caché.

La comparación de los valores del criterio complejo en los periodos de optimización y el periodo futuro adopta la siguiente forma:

Criterio complejo para periodos de optimización y pruebas forward

Criterio complejo para periodos de optimización y pruebas forward

Aquí está la estabilidad de los beneficios a plazo.

Beneficio en periodos de optimización y pruebas forward

Beneficio en periodos de optimización y pruebas forward

De los 5952 resultados positivos de la historia, sólo 2655 (también alrededor del 45 %) se mantuvieron en negro. Pero de los 1000 primeros, 581 resultaron tener éxito en forward.

Así pues, hemos visto que es bastante sencillo utilizar OnTester desde el punto de vista técnico, pero nuestro criterio funciona peor que el integrado (ceteris paribus), aunque dista mucho de ser ideal. Así pues, desde el punto de vista de la búsqueda de la fórmula del propio criterio y la posterior elección razonable de los parámetros sin mirar hacia el futuro, hay más preguntas sobre el contenido de OnTester que respuestas.

Aquí, la programación desemboca sin problemas en la investigación y la actividad científica, y queda fuera del alcance de este libro. Pero daremos un ejemplo de criterio calculado con nuestra propia métrica, y no con métricas ya hechas: TesterStatistics. Hablaremos del criterio R2, también conocido como coeficiente de determinación (RSquared.mqh).

Vamos a crear una función para calcular R2 a partir de la curva de equilibrio. Se sabe que cuando se negocia con un lote permanente, un sistema de trading ideal debería mostrar el saldo en forma de línea recta. Ahora utilizamos un lote permanente, por lo que nos vendrá bien. En cuanto a R2 en el caso de lotes variables, lo trataremos un poco más adelante.

Al final, R2 es una medida inversa de la varianza de los datos en relación con la regresión lineal construida sobre ellos. El rango de valores de R2 va de menos infinito a +1 (aunque en nuestro caso es muy improbable que haya grandes valores negativos). Es obvio que la línea encontrada se caracteriza simultáneamente por una pendiente; por lo tanto, para universalizar el código, guardaremos tanto R2 como la tangente del ángulo en la estructura R2A como resultado intermedio.

struct R2A
{
   double r2;    // square of correlation coefficient
   double angle// tangent of the slope
   R2A(): r2(0), angle(0) { }
};

El cálculo de los indicadores se realiza en la función RSquared que toma un array de datos como entrada y devuelve una estructura R2A.

R2A RSquared(const double &data[])
{
   int size = ArraySize(data);
   if(size <= 2return R2A();
   double xydiv;
   int k = 0;
   double Sx = 0Sy = 0Sxy = 0Sx2 = 0Sy2 = 0;
   for(int i = 0i < size; ++i)
   {
      if(data[i] == EMPTY_VALUE
      || !MathIsValidNumber(data[i])) continue;
      x = i + 1;
      y = data[i];
      Sx  += x;
      Sy  += y;
      Sxy += x * y;
      Sx2 += x * x;
      Sy2 += y * y;
      ++k;
   }
   size = k;
   const double Sx22 = Sx * Sx / size;
   const double Sy22 = Sy * Sy / size;
   const double SxSy = Sx * Sy / size;
   div = (Sx2 - Sx22) * (Sy2 - Sy22);
   if(fabs(div) < DBL_EPSILONreturn R2A();
   R2A result;
   result.r2 = (Sxy - SxSy) * (Sxy - SxSy) / div;
   result.angle = (Sxy - SxSy) / (Sx2 - Sx22);
   return result;
}

Para la optimización necesitamos un valor de criterio, y aquí el ángulo es importante porque una curva de equilibrio descendente suave con una pendiente negativa también puede obtener una buena estimación R2. Por lo tanto, escribiremos una función más que «sumará menos» a cualquier estimación de R2 con un ángulo negativo. Tomamos el valor de R2 módulo porque puede ser negativo en el caso de datos muy malos (dispersos) que no encajan en nuestro modelo lineal. Por lo tanto, debemos evitar una situación en la que un menos por un menos dé un más.

double RSquaredTest(const double &data[])
{
   const R2A result = RSquared(data);
   const double weight = 1.0 - 1.0 / sqrt(ArraySize(data) + 1);
   if(result.angle < 0return -fabs(result.r2) * weight;
   return result.r2 * weight;
}

Además, nuestro criterio tiene en cuenta el tamaño de la serie, que corresponde al número de operaciones. Por ello, un aumento del número de transacciones incrementará el indicador.

Teniendo esta herramienta a nuestra disposición, implementaremos la función de calcular la línea de balance en el Asesor Experto y encontraremos R2 para ello. Al final, multiplicamos el valor por 100, convirtiendo así la escala al rango del criterio complejo integrado.

#define STAT_PROPS 4
   
double GetR2onBalanceCurve()
{
   HistorySelect(0LONG_MAX);
   
   const ENUM_DEAL_PROPERTY_DOUBLE props[STAT_PROPS] =
   {
      DEAL_PROFITDEAL_SWAPDEAL_COMMISSIONDEAL_FEE
   };
   double expenses[][STAT_PROPS];
   ulong tickets[]; // only needed because of the 'select' prototype, but useful for debugging
   
   DealFilter filter;
   filter.let(DEAL_TYPE, (1 << DEAL_TYPE_BUY) | (1 << DEAL_TYPE_SELL), IS::OR_BITWISE)
      .let(DEAL_ENTRY,
      (1 << DEAL_ENTRY_OUT) | (1 << DEAL_ENTRY_INOUT) | (1 << DEAL_ENTRY_OUT_BY),
      IS::OR_BITWISE)
      .select(propsticketsexpenses);
   
   const int n = ArraySize(tickets);
   
   double balance[];
   
   ArrayResize(balancen + 1);
   balance[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
   
   for(int i = 0i < n; ++i)
   {
      double result = 0;
      for(int j = 0j < STAT_PROPS; ++j)
      {
         result += expenses[i][j];
      }
      balance[i + 1] = result + balance[i];
   }
   const double r2 = RSquaredTest(balance);
   return r2 * 100;
}

En el manejador OnTester, utilizaremos el nuevo criterio bajo la directiva de compilación condicional, por lo que necesitamos descomentar la directiva #define USE_R2_CRITERION al principio del código fuente.

double OnTester()
{
#ifdef USE_R2_CRITERION
   return GetR2onBalanceCurve();
#else
   const double profit = TesterStatistics(STAT_PROFIT);
   return sign(profit) * sqrt(fabs(profit))
      * sqrt(TesterStatistics(STAT_PROFIT_FACTOR))
      * sqrt(TesterStatistics(STAT_TRADES))
      * sqrt(fabs(TesterStatistics(STAT_SHARPE_RATIO)));
#endif      
}

Vamos a borrar los resultados anteriores de las optimizaciones (archivos opt con caché) y a lanzar una nueva optimización del Asesor Experto: por el criterio R2.

Al comparar los valores del criterio R2 con los del criterio complejo, podemos afirmar que ha aumentado la «convergencia» entre ambos.

Comparación del criterio personalizado R2 y el criterio integrado complejo

Comparación del criterio personalizado R2 y el criterio integrado complejo

Los valores del criterio R2 en la ventana de optimización y en el periodo anterior para los conjuntos de parámetros correspondientes tienen el siguiente aspecto:

Criterio R2 sobre los periodos de optimización y las pruebas forward

Criterio R2 sobre los periodos de optimización y las pruebas forward

Y así es como se combinan los beneficios en el pasado y en el futuro:

Beneficio en periodos de optimización y pruebas forward

Beneficio en periodos de optimización y pruebas forward para R2

Las estadísticas son las siguientes: de los últimos 5582 pases rentables, 2638 (47 %) siguieron siendo rentables, y de los 1000 primeros pases más rentables hay 566 que siguieron siendo rentables, lo que es comparable con el criterio de complejidad integrado.

Como ya se ha dicho, las estadísticas proporcionan materia prima para las siguientes fases de optimización, más inteligentes, que son algo más que una tarea de programación. Nos centraremos en otros aspectos puramente programáticos de la optimización.